ultimate-jekyll-manager 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/CLAUDE-ATTRIBUTION.md +215 -0
  3. package/CLAUDE.md +7 -6
  4. package/README.md +1 -0
  5. package/dist/assets/css/pages/test/libraries/layers/index.scss +28 -0
  6. package/dist/assets/js/core/auth.js +24 -39
  7. package/dist/assets/js/modules/redirect.js +5 -4
  8. package/dist/assets/js/pages/download/index.js +1 -1
  9. package/dist/assets/js/pages/feedback/index.js +7 -1
  10. package/dist/assets/js/pages/test/libraries/layers/index.js +11 -0
  11. package/dist/assets/themes/_template/README.md +50 -0
  12. package/dist/assets/themes/_template/_config.scss +60 -0
  13. package/dist/assets/themes/_template/_theme.js +13 -4
  14. package/dist/assets/themes/_template/_theme.scss +16 -4
  15. package/dist/assets/themes/_template/css/base/_root.scss +19 -0
  16. package/dist/assets/themes/_template/css/components/_components.scss +23 -0
  17. package/dist/assets/themes/classy/README.md +18 -6
  18. package/dist/assets/themes/neobrutalism/README.md +98 -0
  19. package/dist/assets/themes/neobrutalism/_config.scss +139 -0
  20. package/dist/assets/themes/neobrutalism/_theme.js +27 -0
  21. package/dist/assets/themes/neobrutalism/_theme.scss +33 -0
  22. package/dist/assets/themes/neobrutalism/css/base/_mixins.scss +46 -0
  23. package/dist/assets/themes/neobrutalism/css/base/_root.scss +80 -0
  24. package/dist/assets/themes/neobrutalism/css/base/_typography.scss +77 -0
  25. package/dist/assets/themes/neobrutalism/css/base/_utilities.scss +25 -0
  26. package/dist/assets/themes/neobrutalism/css/components/_buttons.scss +148 -0
  27. package/dist/assets/themes/neobrutalism/css/components/_cards.scss +69 -0
  28. package/dist/assets/themes/neobrutalism/css/components/_forms.scss +88 -0
  29. package/dist/assets/themes/neobrutalism/css/components/_infinite-scroll.scss +94 -0
  30. package/dist/assets/themes/neobrutalism/css/layout/_general.scss +200 -0
  31. package/dist/assets/themes/neobrutalism/css/layout/_navigation.scss +153 -0
  32. package/dist/assets/themes/neobrutalism/js/initialize-tooltips.js +20 -0
  33. package/dist/assets/themes/neobrutalism/js/navbar-scroll.js +29 -0
  34. package/dist/assets/themes/neobrutalism/pages/index.scss +227 -0
  35. package/dist/assets/themes/neobrutalism/pages/pricing/index.scss +267 -0
  36. package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.js +9 -0
  37. package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.scss +7 -0
  38. package/dist/build.js +2 -5
  39. package/dist/commands/install.js +1 -1
  40. package/dist/commands/setup.js +41 -0
  41. package/dist/defaults/CLAUDE.md +5 -1
  42. package/dist/defaults/dist/_alternatives/example-competitor.md +6 -6
  43. package/dist/defaults/dist/_includes/admin/sections/sidebar.json +2 -2
  44. package/dist/defaults/dist/_includes/core/head.html +17 -0
  45. package/dist/defaults/dist/_includes/themes/classy/backend/sections/topbar.html +1 -1
  46. package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +9 -6
  47. package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +13 -13
  48. package/dist/defaults/dist/_layouts/blueprint/admin/firebase/index.html +1 -1
  49. package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +1 -1
  50. package/dist/defaults/dist/_layouts/blueprint/admin/users/new.html +5 -5
  51. package/dist/defaults/dist/_layouts/blueprint/auth/oauth2.html +1 -1
  52. package/dist/defaults/dist/_layouts/themes/classy/backend/pages/dashboard/index.html +12 -12
  53. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/about.html +1 -1
  54. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html +4 -4
  55. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/index.html +5 -5
  56. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +4 -2
  57. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +7 -3
  58. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/confirmation.html +1 -1
  59. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +3 -3
  60. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/team/index.html +2 -2
  61. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/core/base.html +31 -0
  62. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/index.html +345 -0
  63. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/pricing.html +483 -0
  64. package/dist/defaults/dist/_updates/v0.0.1.md +3 -0
  65. package/dist/defaults/dist/pages/test/account/dashboard.html +1 -1
  66. package/dist/defaults/dist/pages/test/libraries/ads.html +9 -9
  67. package/dist/defaults/dist/pages/test/libraries/bootstrap.html +6 -6
  68. package/dist/defaults/dist/pages/test/libraries/firestore.html +1 -1
  69. package/dist/defaults/dist/pages/test/libraries/form-manager.html +2 -2
  70. package/dist/defaults/dist/pages/test/libraries/layers.html +57 -0
  71. package/dist/defaults/dist/pages/test/libraries/lazy-loading.html +8 -8
  72. package/dist/defaults/dist/sitemap.html +2 -2
  73. package/dist/defaults/src/_config.yml +2 -0
  74. package/dist/defaults/test/_init.js +10 -0
  75. package/dist/gulp/tasks/defaults.js +8 -0
  76. package/dist/gulp/tasks/imagemin.js +30 -5
  77. package/dist/gulp/tasks/sass.js +43 -2
  78. package/dist/gulp/tasks/translation.js +11 -0
  79. package/dist/gulp/tasks/utils/manage-test-layers.js +97 -0
  80. package/dist/index.js +30 -4
  81. package/dist/test/runner.js +62 -0
  82. package/dist/test/suites/build/manager.test.js +11 -4
  83. package/dist/test/suites/build/mode-helpers.test.js +54 -2
  84. package/dist/utils/attach-log-file.js +24 -16
  85. package/dist/utils/mode-helpers.js +65 -40
  86. package/docs/assets.md +6 -1
  87. package/docs/environment-detection.md +85 -0
  88. package/docs/test-framework.md +48 -3
  89. package/docs/themes.md +451 -0
  90. package/package.json +2 -1
  91. package/docs/cross-context-helpers.md +0 -75
@@ -11,7 +11,7 @@ module.exports = {
11
11
  name: 'helpers attach to Manager statically AND on prototype',
12
12
  run: async (ctx) => {
13
13
  const Manager = require('../../../build.js');
14
- for (const name of ['isTesting', 'isDevelopment', 'isProduction', 'getVersion']) {
14
+ for (const name of ['getEnvironment', 'isTesting', 'isDevelopment', 'isProduction', 'getVersion']) {
15
15
  ctx.expect(typeof Manager[name]).toBe('function');
16
16
  ctx.expect(typeof Manager.prototype[name]).toBe('function');
17
17
  }
@@ -35,17 +35,69 @@ module.exports = {
35
35
  },
36
36
  },
37
37
  {
38
- name: 'isDevelopment false when UJ_BUILD_MODE=true',
38
+ name: 'isDevelopment false / isProduction true when UJ_BUILD_MODE=true (and not testing)',
39
39
  run: async (ctx) => {
40
40
  const Manager = require('../../../build.js');
41
41
  const original = process.env.UJ_BUILD_MODE;
42
+ const origTest = process.env.UJ_TEST_MODE;
42
43
  try {
44
+ delete process.env.UJ_TEST_MODE; // isolate the build-mode branch from testing precedence
43
45
  process.env.UJ_BUILD_MODE = 'true';
44
46
  ctx.expect(Manager.isDevelopment()).toBe(false);
45
47
  ctx.expect(Manager.isProduction()).toBe(true);
46
48
  } finally {
47
49
  if (original === undefined) delete process.env.UJ_BUILD_MODE;
48
50
  else process.env.UJ_BUILD_MODE = original;
51
+ if (origTest !== undefined) process.env.UJ_TEST_MODE = origTest;
52
+ }
53
+ },
54
+ },
55
+ {
56
+ name: 'environments are mutually exclusive — testing wins under UJ_TEST_MODE',
57
+ run: async (ctx) => {
58
+ const Manager = require('../../../build.js');
59
+ // These tests run under UJ_TEST_MODE=true → testing wins; dev and prod are false.
60
+ ctx.expect(Manager.isTesting()).toBe(true);
61
+ ctx.expect(Manager.isDevelopment()).toBe(false);
62
+ ctx.expect(Manager.isProduction()).toBe(false);
63
+ },
64
+ },
65
+ {
66
+ // The core invariant of the SSOT refactor: is*() DERIVE from getEnvironment(), so they
67
+ // can NEVER disagree with it, and exactly one is always true. (In build-time Node `window`
68
+ // is undefined, so getEnvironment() resolves via the env-var fallback.)
69
+ name: 'invariant: is*() exactly matches getEnvironment() + mutually exclusive (every scenario)',
70
+ run: async (ctx) => {
71
+ const Manager = require('../../../build.js');
72
+ const prevTest = process.env.UJ_TEST_MODE;
73
+ const prevBuild = process.env.UJ_BUILD_MODE;
74
+ const prevServer = process.env.UJ_IS_SERVER;
75
+ const prevNode = process.env.NODE_ENV;
76
+ const scenarios = [
77
+ { env: { UJ_TEST_MODE: 'true', UJ_BUILD_MODE: 'true' }, expect: 'testing' },
78
+ { env: { UJ_BUILD_MODE: 'true' }, expect: 'production' },
79
+ { env: { UJ_IS_SERVER: 'true' }, expect: 'production' },
80
+ { env: { NODE_ENV: 'development' }, expect: 'development' },
81
+ { env: {}, expect: 'development' }, // UJM defaults dev (signal always baked in)
82
+ ];
83
+ try {
84
+ for (const s of scenarios) {
85
+ delete process.env.UJ_TEST_MODE; delete process.env.UJ_BUILD_MODE;
86
+ delete process.env.UJ_IS_SERVER; delete process.env.NODE_ENV;
87
+ for (const k of Object.keys(s.env)) process.env[k] = s.env[k];
88
+ const e = Manager.getEnvironment();
89
+ ctx.expect(e).toBe(s.expect);
90
+ ctx.expect(Manager.isDevelopment()).toBe(e === 'development');
91
+ ctx.expect(Manager.isTesting()).toBe(e === 'testing');
92
+ ctx.expect(Manager.isProduction()).toBe(e === 'production');
93
+ const trueCount = [Manager.isDevelopment(), Manager.isTesting(), Manager.isProduction()].filter(Boolean).length;
94
+ ctx.expect(trueCount).toBe(1);
95
+ }
96
+ } finally {
97
+ if (prevTest === undefined) delete process.env.UJ_TEST_MODE; else process.env.UJ_TEST_MODE = prevTest;
98
+ if (prevBuild === undefined) delete process.env.UJ_BUILD_MODE; else process.env.UJ_BUILD_MODE = prevBuild;
99
+ if (prevServer === undefined) delete process.env.UJ_IS_SERVER; else process.env.UJ_IS_SERVER = prevServer;
100
+ if (prevNode === undefined) delete process.env.NODE_ENV; else process.env.NODE_ENV = prevNode;
49
101
  }
50
102
  },
51
103
  },
@@ -9,18 +9,24 @@
9
9
  // Skipped entirely when Manager.isServer() returns true — CI/cloud runs don't need a logs/
10
10
  // directory left behind in the workspace.
11
11
  //
12
- // Truncates fresh on each call (flags: 'w'), so a new `npm start` doesn't accumulate stale
12
+ // Truncates fresh on each call (O_TRUNC), so a new `npm start` doesn't accumulate stale
13
13
  // lines from the previous run.
14
14
  //
15
- // Idempotent: calling twice with the same path just returns the existing stream.
15
+ // Idempotent: calling twice with the same path just returns the existing fd.
16
+ //
17
+ // Uses synchronous fs.writeSync(fd, ...) rather than createWriteStream(). Reason: gulp tasks
18
+ // crash via thrown errors that propagate to process.exit, and createWriteStream's internal
19
+ // buffer was being dropped before the kernel could flush it — so the very lines describing
20
+ // the crash (the most important ones) never made it to disk. Synchronous writes incur a
21
+ // per-line syscall but guarantee the tail of the log survives an immediate exit.
16
22
 
17
23
  const fs = require('fs');
18
24
  const path = require('path');
19
25
 
20
26
  const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
21
27
 
22
- let activeStream = null;
23
- let activePath = null;
28
+ let activeFd = null;
29
+ let activePath = null;
24
30
  let originalStdoutWrite = null;
25
31
  let originalStderrWrite = null;
26
32
 
@@ -33,38 +39,40 @@ function attachLogFile(name) {
33
39
 
34
40
  const abs = path.resolve(process.cwd(), 'logs', `${name}.log`);
35
41
 
36
- if (activeStream && activePath === abs) return activeStream;
37
- if (activeStream) detach();
42
+ if (activeFd !== null && activePath === abs) return activeFd;
43
+ if (activeFd !== null) detach();
38
44
 
39
45
  fs.mkdirSync(path.dirname(abs), { recursive: true });
40
- const stream = fs.createWriteStream(abs, { flags: 'w' });
46
+ const fd = fs.openSync(abs, 'w');
41
47
 
42
- stream.write(`# ujm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
48
+ fs.writeSync(fd, `# ujm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
43
49
 
44
50
  originalStdoutWrite = process.stdout.write.bind(process.stdout);
45
51
  originalStderrWrite = process.stderr.write.bind(process.stderr);
46
52
 
47
53
  process.stdout.write = function (chunk, ...rest) {
48
- try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
54
+ try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
49
55
  return originalStdoutWrite(chunk, ...rest);
50
56
  };
51
57
  process.stderr.write = function (chunk, ...rest) {
52
- try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
58
+ try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
53
59
  return originalStderrWrite(chunk, ...rest);
54
60
  };
55
61
 
56
- activeStream = stream;
57
- activePath = abs;
62
+ activeFd = fd;
63
+ activePath = abs;
58
64
 
59
- return stream;
65
+ return fd;
60
66
  }
61
67
 
62
68
  function detach() {
63
69
  if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
64
70
  if (originalStderrWrite) process.stderr.write = originalStderrWrite;
65
- if (activeStream) activeStream.end();
66
- activeStream = null;
67
- activePath = null;
71
+ if (activeFd !== null) {
72
+ try { fs.closeSync(activeFd); } catch (e) { /* ignore */ }
73
+ }
74
+ activeFd = null;
75
+ activePath = null;
68
76
  originalStdoutWrite = null;
69
77
  originalStderrWrite = null;
70
78
  }
@@ -2,51 +2,72 @@
2
2
  // (build-time `src/build.js`, frontend ES module `src/index.js`, service worker
3
3
  // `src/service-worker.js`).
4
4
  //
5
- // Three orthogonal concepts:
6
- // isDevelopment() true when running in dev mode (jekyll dev server, not a
7
- // production build). Detected via NODE_ENV / UJ_BUILD_MODE /
8
- // site config.
9
- // isProduction() — inverse. Running a production-built `_site/`.
10
- // isTesting() — true when UJM's test framework is running this process. Set
11
- // by UJM's test runners (UJ_TEST_MODE=true) and consumer test
12
- // setups that want the same signal.
5
+ // `getEnvironment()` is the SINGLE SOURCE OF TRUTH: it is the ONLY function that reads the
6
+ // raw signals (UJ_TEST_MODE / window.Configuration.uj.environment / UJ_BUILD_MODE /
7
+ // UJ_IS_SERVER / NODE_ENV) and resolves them to exactly ONE of three mutually-exclusive
8
+ // values. The three is*() checks DERIVE from it — they never read raw signals themselves,
9
+ // so they can never disagree with getEnvironment().
13
10
  //
14
- // Use these whenever behavior should differ by *what kind of process* you're in —
15
- // shorter timeouts in tests, prompts suppressed in tests, dev-only banners.
16
- // Don't use them for "should we hit dev or prod backends" — that's a config
17
- // concern; use `getEnvironment()` for that (in build.js).
11
+ // isDevelopment() `getEnvironment() === 'development'`: running in dev mode (jekyll dev
12
+ // server, not a production build), and NOT testing.
13
+ // isTesting() — `getEnvironment() === 'testing'`: UJM's test framework is running this
14
+ // process (UJ_TEST_MODE=true). TAKES PRECEDENCE a test run is not dev.
15
+ // isProduction() — `getEnvironment() === 'production'`: running a production-built `_site/`,
16
+ // and NOT testing. A real positive check — NOT `!isDevelopment()`.
18
17
  //
19
- // Context caveat: in build-time Node (gulp / CLI), `window` is undefined. We
20
- // detect via `typeof window` so the same code works in every context. In test
21
- // mode the browser-side check is short-circuited via UJ_TEST_MODE / global so
22
- // `isTesting()` returns a stable value regardless of which test layer is running.
18
+ // To gate "anything non-production" use `!isProduction()` or `isDevelopment() ||
19
+ // isTesting()` intentionally never assume two values.
20
+ //
21
+ // Context caveat: in build-time Node (gulp / CLI), `window` is undefined. getEnvironment()
22
+ // detects via `typeof window` so the same code works in every context. Browser detection
23
+ // reads `window.Configuration.uj.environment` (baked into the page at build time).
23
24
 
24
- function isDevelopment() {
25
- // Build-time Node fallback.
25
+ // getEnvironment() — the SINGLE SOURCE OF TRUTH. Reads every raw signal and resolves to
26
+ // exactly ONE of 'development' | 'testing' | 'production' (mutually exclusive; testing wins).
27
+ // Precedence: testing → production → development.
28
+ function getEnvironment() {
29
+ // 1. Testing wins — set by UJM's test runners / harness, or a testing-baked build.
30
+ // Works in Node (process.env), browser (globalThis set before consumer JS), and
31
+ // config-baked builds (window.Configuration.uj.environment === 'testing').
32
+ if (typeof process !== 'undefined' && process.env && process.env.UJ_TEST_MODE === 'true') return 'testing';
33
+ if (typeof globalThis !== 'undefined' && globalThis.UJ_TEST_MODE === true) return 'testing';
34
+ if (typeof window !== 'undefined' && window.Configuration && window.Configuration.uj
35
+ && window.Configuration.uj.environment === 'testing') return 'testing';
36
+
37
+ // 2. Build-time Node signals (a production build or the running dev server).
26
38
  if (typeof process !== 'undefined' && process.env) {
27
- if (process.env.UJ_BUILD_MODE === 'true') return false;
28
- if (process.env.NODE_ENV === 'development') return true;
29
- if (process.env.UJ_IS_SERVER === 'true') return false;
39
+ if (process.env.UJ_BUILD_MODE === 'true') return 'production';
40
+ if (process.env.UJ_IS_SERVER === 'true') return 'production';
41
+ if (process.env.NODE_ENV === 'development') return 'development';
30
42
  }
31
- // Browser-side: look at window.Configuration if present.
43
+
44
+ // 3. Browser-side: the environment baked into the page at build time.
32
45
  if (typeof window !== 'undefined' && window.Configuration && window.Configuration.uj) {
33
- if (window.Configuration.uj.environment === 'development') return true;
34
- if (window.Configuration.uj.environment === 'production') return false;
46
+ if (window.Configuration.uj.environment === 'development') return 'development';
47
+ if (window.Configuration.uj.environment === 'production') return 'production';
35
48
  }
36
- return false;
49
+
50
+ // 4. Default: development. UJM's deployed artifacts ALWAYS carry their signal — the
51
+ // browser has `window.Configuration.uj.environment` baked in, and build-time Node
52
+ // always sets UJ_BUILD_MODE / UJ_IS_SERVER. So reaching here means a bare tooling /
53
+ // local-script context, where development is the sensible answer. (Contrast BEM/EM,
54
+ // whose deployed RUNTIME can legitimately lack a signal, so they default to production.)
55
+ return 'development';
56
+ }
57
+
58
+ // The three checks DERIVE from getEnvironment() — they never read raw signals, so they can
59
+ // never disagree with it. isDevelopment() is NOT true in testing; isProduction() is a real
60
+ // positive check (never `!isDevelopment()`).
61
+ function isDevelopment() {
62
+ return getEnvironment() === 'development';
37
63
  }
38
64
 
39
65
  function isProduction() {
40
- return !this.isDevelopment();
66
+ return getEnvironment() === 'production';
41
67
  }
42
68
 
43
69
  function isTesting() {
44
- // Canonical signal — set by UJM's test runners and consumer test setups alike.
45
- // Works in Node (process.env) AND in browser contexts (the harness preload sets
46
- // globalThis.UJ_TEST_MODE before any consumer code runs).
47
- if (typeof process !== 'undefined' && process.env && process.env.UJ_TEST_MODE === 'true') return true;
48
- if (typeof globalThis !== 'undefined' && globalThis.UJ_TEST_MODE === true) return true;
49
- return false;
70
+ return getEnvironment() === 'testing';
50
71
  }
51
72
 
52
73
  // `getVersion()` — returns UJM's own version string.
@@ -64,19 +85,23 @@ function getVersion() {
64
85
 
65
86
  // Mix the helpers into a Manager constructor's prototype + the constructor itself
66
87
  // (so `Manager.isTesting()` works statically too, matching BEM/EM/BXM pattern).
88
+ // getEnvironment() is the SSOT and is attached here too — build.js no longer defines it.
67
89
  function attachTo(Manager) {
68
- Manager.prototype.isDevelopment = isDevelopment;
69
- Manager.prototype.isProduction = isProduction;
70
- Manager.prototype.isTesting = isTesting;
71
- Manager.prototype.getVersion = getVersion;
72
- Manager.isDevelopment = isDevelopment;
73
- Manager.isProduction = isProduction;
74
- Manager.isTesting = isTesting;
75
- Manager.getVersion = getVersion;
90
+ Manager.prototype.getEnvironment = getEnvironment;
91
+ Manager.prototype.isDevelopment = isDevelopment;
92
+ Manager.prototype.isProduction = isProduction;
93
+ Manager.prototype.isTesting = isTesting;
94
+ Manager.prototype.getVersion = getVersion;
95
+ Manager.getEnvironment = getEnvironment;
96
+ Manager.isDevelopment = isDevelopment;
97
+ Manager.isProduction = isProduction;
98
+ Manager.isTesting = isTesting;
99
+ Manager.getVersion = getVersion;
76
100
  }
77
101
 
78
102
  module.exports = {
79
103
  attachTo,
104
+ getEnvironment,
80
105
  isDevelopment,
81
106
  isProduction,
82
107
  isTesting,
package/docs/assets.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  This document covers UJM's asset layout, consuming-project overrides, the JSON-driven section configuration system (nav, footer, account dropdown), frontmatter-driven page customization, webpack aliases, and the page module pattern.
4
4
 
5
+ > **Authoring or selecting a theme?** The `themes/[theme-id]/` layout/include
6
+ > paths and the `__theme__` SCSS/JS resolution referenced throughout this doc are
7
+ > explained in full — including the classy layout fallback and how to build a new
8
+ > theme — in [docs/themes.md](themes.md).
9
+
5
10
  ## Ultimate Jekyll Manager Files (THIS project)
6
11
 
7
12
  **CSS:**
@@ -269,7 +274,7 @@ import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js
269
274
  import(`__project_assets__/js/pages/${pageModulePath}`)
270
275
  ```
271
276
 
272
- **How they work together:** `src/index.js` loads page modules from both aliases first from `__main_assets__` (UJM defaults), then from `__project_assets__` (project overrides/extensions). If a project module doesn't exist, it gracefully skips. This enables a layered system where UJM provides defaults and consuming projects can extend or override page behavior.
277
+ **How they work together:** `src/index.js` loads page modules from **three** layers, executed in order: `__main_assets__` (UJM default) `__theme__/pages/...` (active theme) → `__project_assets__` (consumer override/extension). A missing module at any layer gracefully skips. This layered system mirrors the page-CSS cascade — UJM provides defaults, the theme customizes, and the consuming project always gets the final say. See [docs/themes.md → Page-specific JS](themes.md#5-page-specific-js-theme-aware-additive--mirrors-page-css).
273
278
 
274
279
  **When to use which:**
275
280
  - **`__main_assets__`** — When importing UJM-provided libraries, core modules, or referencing UJM's built-in page scripts
@@ -0,0 +1,85 @@
1
+ # Environment Detection
2
+
3
+ `getEnvironment()` returns exactly ONE of three mutually-exclusive, exhaustive values:
4
+
5
+ ```javascript
6
+ Manager.getEnvironment() // 'development' | 'testing' | 'production'
7
+
8
+ Manager.isDevelopment() // true ONLY in development
9
+ Manager.isTesting() // true ONLY in testing
10
+ Manager.isProduction() // true ONLY in production
11
+ ```
12
+
13
+ **The Manager is the single source of truth.** `getEnvironment()` is the ONLY function that reads the raw signals (`UJ_TEST_MODE` / `window.Configuration.uj.environment` / `UJ_BUILD_MODE` / `UJ_IS_SERVER` / `NODE_ENV`). The three `is*()` checks **derive** from it live on every call — they never read raw signals themselves, so they can never disagree with `getEnvironment()`.
14
+
15
+ **One implementation, mixed into every Manager.** UJM mixes the helpers into the build-time Manager and the frontend Manager via `attachTo(Manager)` from [src/utils/mode-helpers.js](../src/utils/mode-helpers.js), available as both prototype methods (`manager.isTesting()`) and statics (`Manager.isTesting()`).
16
+
17
+ ```javascript
18
+ manager.getEnvironment() // same answer build-time and in the browser
19
+ Manager.isTesting() // static form, for build-time scripts
20
+ ```
21
+
22
+ **Resolution order:** testing wins first, then production, else development. The three checks are mutually exclusive — exactly one is true. `isDevelopment()` is **false** during testing, and `isProduction()` is a real positive check (it is NOT `!isDevelopment()`).
23
+
24
+ ## Available helpers
25
+
26
+ | Helper | Returns |
27
+ |---|---|
28
+ | `getEnvironment()` | `'development' \| 'testing' \| 'production'` — the SSOT resolver; the only reader of raw signals. |
29
+ | `isDevelopment()` | `true` ONLY in development (jekyll dev server, not a production build), and NOT testing. Derives from `getEnvironment()`. |
30
+ | `isTesting()` | `true` ONLY in testing (`UJ_TEST_MODE === 'true'`). **Takes precedence** — a test run is not development. |
31
+ | `isProduction()` | `true` ONLY in production (a production-built `_site/`). A **real positive check** — NOT `!isDevelopment()`. |
32
+
33
+ ## Gating side effects — use the INTENTIONAL check
34
+
35
+ Because there are three environments, never gate a side effect on a two-value assumption. State what you mean:
36
+
37
+ ```javascript
38
+ // Production-only (skip real telemetry / production behavior in dev AND testing):
39
+ if (isProduction()) { /* do the real thing */ }
40
+ if (!isProduction()) { /* skip / use the safe local behavior */ }
41
+
42
+ // Local-or-test (anything that should run in BOTH dev and testing):
43
+ if (isDevelopment() || isTesting()) { /* localhost URL, observable redirect delay, dev banners */ }
44
+ ```
45
+
46
+ **Avoid** `if (!isDevelopment())` or `if (env !== 'development')` to gate production behavior — those wrongly include `testing` as production and leak real side effects during test runs. This is the bug class that motivated the 3-value model.
47
+
48
+ ## URL helpers
49
+
50
+ UJM does **not** own backend URL helpers. Frontend page code resolves backend URLs through the `web-manager` runtime singleton:
51
+
52
+ ```javascript
53
+ webManager.getApiUrl() // the brand's API URL — from web-manager, not UJM
54
+ ```
55
+
56
+ `web-manager`'s `getApiUrl()` follows the same convention as the sister frameworks — local in development/testing, production otherwise — so the rule "call the getter, never hardcode" still applies; the implementation just lives in `web-manager`. UJM's own URL helper is build-time only: `Manager.getWorkingUrl()` returns the BrowserSync dev-server URL (or the configured project URL) for the running dev server.
57
+
58
+ ## Where they live
59
+
60
+ Source: [src/utils/mode-helpers.js](../src/utils/mode-helpers.js) for `getEnvironment()` + `is*()` + `getVersion()`. The module exposes the functions plus an `attachTo(Manager)` mixin. Attached at the bottom of the build-time Manager [src/build.js](../src/build.js) and the frontend Manager [src/index.js](../src/index.js) — so build-time scripts and browser code resolve the environment identically.
61
+
62
+ ## How detection works
63
+
64
+ `getEnvironment()` resolves in this precedence order:
65
+
66
+ 1. **Testing** — `process.env.UJ_TEST_MODE === 'true'`, `globalThis.UJ_TEST_MODE === true`, or a build baked with `window.Configuration.uj.environment === 'testing'` (set by the harness before any consumer JS runs). A test run is a test run regardless of any other signal.
67
+ 2. **Build-time signals** — `UJ_BUILD_MODE === 'true'` → production; `UJ_IS_SERVER === 'true'` → production; `NODE_ENV === 'development'` → development.
68
+ 3. **Browser signal** — `window.Configuration.uj.environment` (`'development'` / `'production'`), baked into the page at build time.
69
+ 4. **Default** — development. UJM's deployed artifacts always carry their signal (the browser has `window.Configuration.uj.environment` baked in; build-time Node always sets `UJ_BUILD_MODE`/`UJ_IS_SERVER`), so reaching here means a bare tooling / local-script context where development is correct. (Contrast BEM/EM, whose deployed *runtime* can legitimately lack a signal, so they default to **production**.)
70
+
71
+ ## Adding a new helper
72
+
73
+ Write the function in [src/utils/mode-helpers.js](../src/utils/mode-helpers.js) (or a new `src/utils/<topic>-helpers.js` module), expose it from `attachTo(Manager)`, and ensure both the build-time and frontend Managers call `attachTo`. For anything environment-derived, derive from `getEnvironment()` rather than reading `process.env` / `window.Configuration` directly, so there is one source of truth and no chance of drift.
74
+
75
+ ## Why this matters
76
+
77
+ **One signal, used everywhere.** The test runner sets `UJ_TEST_MODE=true`; every piece of code that calls `isTesting()` (framework or consumer) then sees `true` — no need to invent a per-module env var.
78
+
79
+ **Sub-modules check the same signal.** When framework code (a network probe, a prompt) needs to skip side effects in tests, it checks `isTesting()` — the same answer the consumer's own code gets. No drift.
80
+
81
+ **`is*()` can never disagree with `getEnvironment()`.** Because the checks derive from the single resolver instead of reading raw signals (`window.Configuration` vs `UJ_BUILD_MODE`), there is exactly one definition of "what environment is this," and a wrong-but-confident gate is structurally impossible.
82
+
83
+ ## See also
84
+
85
+ - [test-framework.md](test-framework.md) — `UJ_TEST_MODE` is set automatically by the test runners; `--integration` gates real external APIs.
@@ -2,6 +2,28 @@
2
2
 
3
3
  UJM ships a built-in three-layer test harness. `npx mgr test` discovers framework suites from `<ujm>/dist/test/suites/**/*.js` and consumer suites from `<cwd>/test/**/*.js`, partitions by `layer`, and runs each layer in the right environment. Same shape as the sister harnesses in EM (electron-manager) and BXM (browser-extension-manager).
4
4
 
5
+ ## 🚫 NEVER mock — test against the real harness (HARD RULE)
6
+
7
+ **Do NOT hand-roll fake/stub/mock objects.** Every test runs against a real environment, and the harness hands the test the real thing — use it:
8
+
9
+ - **`build` layer** gets the **real** `Manager` from `require('ultimate-jekyll-manager/build')` — call its real API (`getConfig`, `getUJMConfig`, `getPackage`, `isTesting`, the gulp pure helpers). Never fake a `Manager` whose `getConfig()` returns canned data.
10
+ - **`page` layer** runs in a **real** headless Chromium tab with real `window`/`document` and the harness-provided `window.Configuration`. Drive the real frontend Manager surface; don't stub DOM globals in your test.
11
+ - **`boot` layer** runs against the **real** built `_site/` served over a **real** HTTP origin — exercise the actually-shipped site through Puppeteer.
12
+ - **Pure functions (zero I/O) are the ONLY thing you call directly** — e.g. `mergeJekyllConfigs`, `validateYAMLFrontMatter`, `createTemplateTransform`, `collectTextNodes`. `require()` them and pass plain inputs. That is NOT mocking — there is nothing to mock. The moment a function touches real I/O (config files, the DOM, the HTTP server, an external API), it MUST run against the real harness/build, not a stub.
13
+
14
+ If you find yourself writing `const mockX = {...}` to satisfy code under test, STOP — use the real context the layer already provides, or (if it's genuinely pure) call it with plain data.
15
+
16
+ ### The ONLY two exceptions where a narrow stub is allowed
17
+
18
+ Mock **nothing** by default. There are exactly two cases where the real dependency genuinely cannot run in the test environment — and even then, stub the *smallest possible seam* (one method / one object), restore it immediately, and comment *why*:
19
+
20
+ 1. **A side effect that would destroy the test run itself.** If the real call would kill or corrupt the harness — a process-exit, a destructive `_site/` wipe, a recursive re-invocation of the build/test command — stub *that one call* to a no-op, assert the surrounding logic, then restore. You are preventing the harness from terminating mid-assertion, not faking behavior.
21
+ 2. **A real dependency the test environment can't provide.** When the real thing only exists from infra you can't stand up in the current layer (an external service with no local equivalent, a second running instance), a unit test may hand minimal inputs to exercise the logic in isolation — but a real-harness test (`page`/`boot`) MUST still cover the wired path where one exists.
22
+
23
+ If you can run it for real, you must. These exceptions are not a license to unit-test in isolation when a real-harness layer would work.
24
+
25
+ **External APIs are skipped in-source, NOT mocked.** UJM build/gulp code that would hit the network (e.g. fetching Firebase auth files) short-circuits in its own source when `Manager.isTesting()` is true — it returns early, it does not return canned/mocked data. See [environment-detection.md](environment-detection.md). If a suite has slower live-integration tests, gate them behind the `--integration` flag (`UJ_TEST_INTEGRATION=1`) and run the real path; anything such a test creates externally MUST be cleaned up by the test (`cleanup`/`inspect` teardown) — the runner does not clean external systems.
26
+
5
27
  ## Quick start
6
28
 
7
29
  ```bash
@@ -135,7 +157,7 @@ The public surface exposed by `require('ultimate-jekyll-manager/build')` include
135
157
  - `Manager.logger(name)` — returns a `Logger` instance
136
158
  - `Manager.require(path)` — escape hatch when you really need a UJM transitive dep
137
159
 
138
- See [docs/cross-context-helpers.md](cross-context-helpers.md) for `isTesting`/`isDevelopment` semantics.
160
+ See [docs/environment-detection.md](environment-detection.md) for `isTesting`/`isDevelopment` semantics.
139
161
 
140
162
  ## Reporter contract — `__UJM_TEST__` JSON-line events
141
163
 
@@ -162,12 +184,35 @@ Same protocol as EM (`__EM_TEST__`) and BXM (`__BXM_TEST__`). One marker per fra
162
184
  - **Excluded**: any directory starting with `_` (handy for shared helpers).
163
185
  - **Framework boot suites** are excluded when the cwd's `package.json#name` is not `ultimate-jekyll-manager` — they target UJM's fixture site, not the consumer's. Consumers write their own boot tests in `<cwd>/test/boot/`.
164
186
 
187
+ ## `test/_init.js` — pre-test lifecycle hook
188
+
189
+ The runner loads an optional `test/_init.js` from **both** test roots — the framework (`<UJM>/test/_init.js`) and the consumer project (`<cwd>/test/_init.js`) — and runs it **once, before any suite** (it is NOT itself run as a test; the `_`-prefix keeps it out of discovery). Mirrors the same hook in BEM/EM/BXM so all four frameworks share one shape.
190
+
191
+ The module **must export a function** — `module.exports = (ctx) => ({ ... })` — called with `{ projectRoot }` and returning the hook object. It may declare:
192
+
193
+ - `async setup({ projectRoot })` — runs once before the suites, e.g. to scaffold a fixture file the boot layer needs.
194
+
195
+ There is **no `cleanup` hook** and **no `accounts` field** (unlike BEM — these frameworks have no auth/user system): tests clean up after themselves, so there is nothing project-level to tear down.
196
+
197
+ ```javascript
198
+ // <cwd>/test/_init.js
199
+ const fs = require('fs');
200
+ const path = require('path');
201
+
202
+ module.exports = ({ projectRoot }) => ({
203
+ async setup() {
204
+ // Seed any fixture a suite needs before it runs.
205
+ fs.mkdirSync(path.join(projectRoot, '.temp'), { recursive: true });
206
+ },
207
+ });
208
+ ```
209
+
165
210
  ## Env vars
166
211
 
167
212
  | Env | Set by | Purpose |
168
213
  |---|---|---|
169
214
  | `UJ_TEST_MODE=true` | `npx mgr test` always | Canonical test signal. `Manager.isTesting()` reads this. Use it to short-circuit network calls / prompts / long timers in code that runs during tests. |
170
- | `UJ_TEST_INTEGRATION=1` | `--integration` flag | Opt-in flag for slower integration tests if your suite has them |
215
+ | `UJ_TEST_INTEGRATION=1` | `--integration` flag | Opt-in flag for slower live-integration tests if your suite has them. These run the **real** external path (NOT mocked); anything they create externally MUST be cleaned up by the test. |
171
216
  | `UJ_TEST_BOOT_PROJECT` | Auto-set when UJM tests itself; else manual | Project root the boot runner uses (its `_site/` is the boot target) |
172
217
  | `UJ_TEST_BOOT_DIR` | Manual | Absolute override for the `_site/` directory. Wins over `UJ_TEST_BOOT_PROJECT/_site` and `<cwd>/_site` |
173
218
  | `UJ_TEST_DEBUG=1` | Manual | Verbose Puppeteer console output piped to the parent stdout |
@@ -179,5 +224,5 @@ Puppeteer is a `devDependency` of UJM itself. Consumers don't get it unless they
179
224
  ## See also
180
225
 
181
226
  - [test-boot-layer.md](test-boot-layer.md) — deep dive on boot layer (`_site/` discovery, HTTP server, fixture vs consumer)
182
- - [cross-context-helpers.md](cross-context-helpers.md) — `Manager.isTesting()` / `isDevelopment()` semantics
227
+ - [environment-detection.md](environment-detection.md) — `Manager.isTesting()` / `isDevelopment()` semantics
183
228
  - [cli.md](cli.md) — CLI surface, env-var conventions