ultimate-jekyll-manager 1.4.3 → 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.
- package/CHANGELOG.md +14 -0
- package/CLAUDE-ATTRIBUTION.md +215 -0
- package/CLAUDE.md +7 -6
- package/README.md +1 -0
- package/dist/assets/css/pages/test/libraries/layers/index.scss +28 -0
- package/dist/assets/js/modules/redirect.js +5 -4
- package/dist/assets/js/pages/download/index.js +1 -1
- package/dist/assets/js/pages/feedback/index.js +7 -1
- package/dist/assets/js/pages/test/libraries/layers/index.js +11 -0
- package/dist/assets/themes/_template/README.md +50 -0
- package/dist/assets/themes/_template/_config.scss +60 -0
- package/dist/assets/themes/_template/_theme.js +13 -4
- package/dist/assets/themes/_template/_theme.scss +16 -4
- package/dist/assets/themes/_template/css/base/_root.scss +19 -0
- package/dist/assets/themes/_template/css/components/_components.scss +23 -0
- package/dist/assets/themes/classy/README.md +18 -6
- package/dist/assets/themes/neobrutalism/README.md +98 -0
- package/dist/assets/themes/neobrutalism/_config.scss +139 -0
- package/dist/assets/themes/neobrutalism/_theme.js +27 -0
- package/dist/assets/themes/neobrutalism/_theme.scss +33 -0
- package/dist/assets/themes/neobrutalism/css/base/_mixins.scss +46 -0
- package/dist/assets/themes/neobrutalism/css/base/_root.scss +80 -0
- package/dist/assets/themes/neobrutalism/css/base/_typography.scss +77 -0
- package/dist/assets/themes/neobrutalism/css/base/_utilities.scss +25 -0
- package/dist/assets/themes/neobrutalism/css/components/_buttons.scss +148 -0
- package/dist/assets/themes/neobrutalism/css/components/_cards.scss +69 -0
- package/dist/assets/themes/neobrutalism/css/components/_forms.scss +88 -0
- package/dist/assets/themes/neobrutalism/css/components/_infinite-scroll.scss +94 -0
- package/dist/assets/themes/neobrutalism/css/layout/_general.scss +200 -0
- package/dist/assets/themes/neobrutalism/css/layout/_navigation.scss +153 -0
- package/dist/assets/themes/neobrutalism/js/initialize-tooltips.js +20 -0
- package/dist/assets/themes/neobrutalism/js/navbar-scroll.js +29 -0
- package/dist/assets/themes/neobrutalism/pages/index.scss +227 -0
- package/dist/assets/themes/neobrutalism/pages/pricing/index.scss +267 -0
- package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.js +9 -0
- package/dist/assets/themes/neobrutalism/pages/test/libraries/layers/index.scss +7 -0
- package/dist/build.js +2 -5
- package/dist/commands/install.js +1 -1
- package/dist/commands/setup.js +41 -0
- package/dist/defaults/CLAUDE.md +5 -1
- package/dist/defaults/dist/_includes/core/head.html +17 -0
- package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +4 -4
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +2 -0
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +7 -3
- package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/core/base.html +31 -0
- package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/index.html +345 -0
- package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/pricing.html +483 -0
- package/dist/defaults/dist/pages/test/libraries/layers.html +57 -0
- package/dist/defaults/src/_config.yml +2 -0
- package/dist/defaults/test/_init.js +10 -0
- package/dist/gulp/tasks/defaults.js +8 -0
- package/dist/gulp/tasks/sass.js +43 -2
- package/dist/gulp/tasks/translation.js +11 -0
- package/dist/gulp/tasks/utils/manage-test-layers.js +97 -0
- package/dist/index.js +30 -4
- package/dist/test/runner.js +62 -0
- package/dist/test/suites/build/manager.test.js +11 -4
- package/dist/test/suites/build/mode-helpers.test.js +54 -2
- package/dist/utils/mode-helpers.js +65 -40
- package/docs/assets.md +6 -1
- package/docs/environment-detection.md +85 -0
- package/docs/test-framework.md +48 -3
- package/docs/themes.md +451 -0
- package/package.json +2 -1
- package/docs/cross-context-helpers.md +0 -75
|
@@ -995,6 +995,11 @@ function getIgnoredPages() {
|
|
|
995
995
|
const socials = config?.socials || {};
|
|
996
996
|
// const downloads = config?.downloads || {};
|
|
997
997
|
|
|
998
|
+
// User-configured excludes (translation.exclude). Each entry can be a folder
|
|
999
|
+
// (e.g. "blog" → /blog/**) or a single page path (e.g. "some-page"). We add
|
|
1000
|
+
// each to BOTH files and folders so it matches either way.
|
|
1001
|
+
const userExcludes = config?.translation?.exclude || [];
|
|
1002
|
+
|
|
998
1003
|
const redirectsDir = path.join('dist', 'redirects');
|
|
999
1004
|
const redirectFiles = glob(`${redirectsDir}/**/*.html`);
|
|
1000
1005
|
const redirectPermalinks = [];
|
|
@@ -1045,6 +1050,9 @@ function getIgnoredPages() {
|
|
|
1045
1050
|
|
|
1046
1051
|
// Redirects
|
|
1047
1052
|
...redirectPermalinks,
|
|
1053
|
+
|
|
1054
|
+
// User-configured excludes (treated as a page path)
|
|
1055
|
+
...userExcludes,
|
|
1048
1056
|
],
|
|
1049
1057
|
folders: [
|
|
1050
1058
|
// Languages
|
|
@@ -1055,6 +1063,9 @@ function getIgnoredPages() {
|
|
|
1055
1063
|
|
|
1056
1064
|
// Firestore auth pages
|
|
1057
1065
|
'__/auth',
|
|
1066
|
+
|
|
1067
|
+
// User-configured excludes (treated as a folder)
|
|
1068
|
+
...userExcludes,
|
|
1058
1069
|
],
|
|
1059
1070
|
};
|
|
1060
1071
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Test-layer manager (dev only)
|
|
2
|
+
// Powers the CONSUMER layer of the /test/libraries/layers asset-cascade panel.
|
|
3
|
+
//
|
|
4
|
+
// The Consumer dots only go green if the consuming project has its own page files
|
|
5
|
+
// at src/assets/{css,js}/pages/test/libraries/layers/index.*. To prove that layer
|
|
6
|
+
// live WITHOUT permanently adding files to the consumer, this is OPT-IN via the
|
|
7
|
+
// UJ_TEST_LAYERS=true env flag:
|
|
8
|
+
//
|
|
9
|
+
// - ALWAYS (every run): remove any previously-generated consumer test-layer files,
|
|
10
|
+
// so they never persist or get committed even if the flag is later unset.
|
|
11
|
+
// - WHEN UJ_TEST_LAYERS=true: (re)generate them into the consumer's src/ at build
|
|
12
|
+
// START — before sass + jekyll run — so the real __project_assets__ / consumer
|
|
13
|
+
// page-CSS path picks them up exactly like any other consumer page file. This is
|
|
14
|
+
// the honest mechanism (no aliases/shims); the files are just real, briefly.
|
|
15
|
+
//
|
|
16
|
+
// Generated files carry a GENERATED marker so the cleaner only ever deletes its own.
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const jetpack = require('fs-jetpack');
|
|
19
|
+
|
|
20
|
+
// Page path the panel lives at → where its consumer-layer assets must sit.
|
|
21
|
+
const REL_CSS = 'src/assets/css/pages/test/libraries/layers/index.scss';
|
|
22
|
+
const REL_JS = 'src/assets/js/pages/test/libraries/layers/index.js';
|
|
23
|
+
|
|
24
|
+
const MARKER = 'GENERATED — UJ_TEST_LAYERS';
|
|
25
|
+
|
|
26
|
+
// NOTE: a consumer page-CSS file shares the SAME output bundle as the framework's
|
|
27
|
+
// base page CSS, so it must @use the base (like every real consumer page file) to
|
|
28
|
+
// COMPOSE with it rather than replace it. The base lives at the same page path under
|
|
29
|
+
// UJM's css/, importable via the SASS loadPaths (which include UJM's dist/assets/css).
|
|
30
|
+
const CSS_CONTENT = `// ${MARKER} (auto-removed on the next build; do not commit)
|
|
31
|
+
// Consumer layer of the /test/libraries/layers panel → turns the "css-consumer" dot green.
|
|
32
|
+
@use 'pages/test/libraries/layers/index' as *;
|
|
33
|
+
|
|
34
|
+
.layer-dot[data-layer="css-consumer"] {
|
|
35
|
+
background: #30a46c; // green
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const JS_CONTENT = `// ${MARKER} (auto-removed on the next build; do not commit)
|
|
40
|
+
// Consumer layer of the /test/libraries/layers panel → turns the "js-consumer" dot green.
|
|
41
|
+
export default ({ manager, options }) => {
|
|
42
|
+
const dot = document.querySelector('.layer-dot[data-layer="js-consumer"]');
|
|
43
|
+
if (dot) {
|
|
44
|
+
dot.style.background = '#30a46c';
|
|
45
|
+
}
|
|
46
|
+
console.log('[test-layer] consumer JS ran → js-consumer dot green');
|
|
47
|
+
};
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
// Delete a generated file only if it still carries our marker (never clobber a real
|
|
51
|
+
// consumer file someone legitimately created at this path).
|
|
52
|
+
function removeIfGenerated(absPath) {
|
|
53
|
+
if (!jetpack.exists(absPath)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const contents = jetpack.read(absPath) || '';
|
|
57
|
+
if (contents.includes(MARKER)) {
|
|
58
|
+
jetpack.remove(absPath);
|
|
59
|
+
// Clean up now-empty generated dirs (best effort)
|
|
60
|
+
jetpack.remove(path.dirname(absPath) + '/.keep'); // no-op if absent
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Manage the consumer-layer test fixtures (dev only).
|
|
68
|
+
* Always cleans prior generated files; generates fresh ones when UJ_TEST_LAYERS=true.
|
|
69
|
+
* @param {object} Manager - UJM build Manager
|
|
70
|
+
* @param {object} logger - task logger (optional)
|
|
71
|
+
*/
|
|
72
|
+
function manageTestLayers(Manager, logger) {
|
|
73
|
+
// Never touch anything in a production build
|
|
74
|
+
if (Manager.isBuildMode()) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const cssPath = path.resolve(process.cwd(), REL_CSS);
|
|
79
|
+
const jsPath = path.resolve(process.cwd(), REL_JS);
|
|
80
|
+
|
|
81
|
+
// 1) Always clean previously-generated files
|
|
82
|
+
const removed = [removeIfGenerated(cssPath), removeIfGenerated(jsPath)].filter(Boolean).length;
|
|
83
|
+
|
|
84
|
+
// 2) Generate when explicitly requested
|
|
85
|
+
const enabled = process.env.UJ_TEST_LAYERS === 'true';
|
|
86
|
+
if (enabled) {
|
|
87
|
+
jetpack.write(cssPath, CSS_CONTENT);
|
|
88
|
+
jetpack.write(jsPath, JS_CONTENT);
|
|
89
|
+
if (logger) {
|
|
90
|
+
logger.log('UJ_TEST_LAYERS: generated consumer test-layer files (auto-removed next run)');
|
|
91
|
+
}
|
|
92
|
+
} else if (removed > 0 && logger) {
|
|
93
|
+
logger.log(`UJ_TEST_LAYERS: cleaned ${removed} stale generated test-layer file(s)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = manageTestLayers;
|
package/dist/index.js
CHANGED
|
@@ -66,10 +66,17 @@ class Manager {
|
|
|
66
66
|
.catch(e => console.error('Failed to load ultimate-jekyll-manager.js:', e))
|
|
67
67
|
);
|
|
68
68
|
|
|
69
|
+
// Theme page module path (resolved via the __theme__ webpack alias)
|
|
70
|
+
const themeModulePathFull = `__theme__/pages/${pageModulePath}`;
|
|
71
|
+
|
|
69
72
|
console.log(`Page-specific module loading: #main/${pageModulePathFull}`);
|
|
73
|
+
console.log(`Page-specific module loading: #theme/${themeModulePathFull}`);
|
|
70
74
|
console.log(`Page-specific module loading: #project/${pageModulePathFull}`);
|
|
71
75
|
|
|
72
|
-
// Load page-specific scripts
|
|
76
|
+
// Load page-specific scripts.
|
|
77
|
+
// Three layers, executed in order: #main (framework default) → #theme
|
|
78
|
+
// (active theme) → #project (consumer). Mirrors the page-CSS cascade.
|
|
79
|
+
// A missing module at any layer is a no-op — no fallback needed.
|
|
73
80
|
modulePromises.push(
|
|
74
81
|
// Import the main page-specific script
|
|
75
82
|
import(`__main_assets__/js/pages/${pageModulePath}`)
|
|
@@ -85,11 +92,29 @@ class Manager {
|
|
|
85
92
|
})
|
|
86
93
|
);
|
|
87
94
|
|
|
95
|
+
modulePromises.push(
|
|
96
|
+
// Import the active theme's page-specific script.
|
|
97
|
+
// webpackInclude restricts the dynamic-import context to .js files — the
|
|
98
|
+
// theme's pages/ dir also contains page CSS (.scss), which must NOT be
|
|
99
|
+
// pulled into the JS context (webpack would try to parse it as JS).
|
|
100
|
+
import(/* webpackInclude: /\.js$/ */ `__theme__/pages/${pageModulePath}`)
|
|
101
|
+
.then(mod => {
|
|
102
|
+
modules[1] = { tag: 'theme', default: mod?.default };
|
|
103
|
+
})
|
|
104
|
+
.catch(e => {
|
|
105
|
+
if (this.isNotFound(e, pageModulePath)) {
|
|
106
|
+
console.warn(`Page-specific module missing: #theme/${themeModulePathFull}`);
|
|
107
|
+
} else {
|
|
108
|
+
console.error(`Page-specific module error: #theme/${themeModulePathFull}`, e);
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
|
|
88
113
|
modulePromises.push(
|
|
89
114
|
// Import the project page-specific script
|
|
90
115
|
import(`__project_assets__/js/pages/${pageModulePath}`)
|
|
91
116
|
.then(mod => {
|
|
92
|
-
modules[
|
|
117
|
+
modules[2] = { tag: 'project', default: mod?.default };
|
|
93
118
|
})
|
|
94
119
|
.catch(e => {
|
|
95
120
|
if (this.isNotFound(e, pageModulePath)) {
|
|
@@ -111,12 +136,13 @@ class Manager {
|
|
|
111
136
|
}
|
|
112
137
|
|
|
113
138
|
// Execute the module function
|
|
139
|
+
const modPathLabel = mod.tag === 'theme' ? themeModulePathFull : pageModulePathFull;
|
|
114
140
|
try {
|
|
115
|
-
console.log(`Page-specific module loaded: #${mod.tag}/${
|
|
141
|
+
console.log(`Page-specific module loaded: #${mod.tag}/${modPathLabel}`);
|
|
116
142
|
|
|
117
143
|
await mod.default({ manager: this, options });
|
|
118
144
|
} catch (e) {
|
|
119
|
-
console.error(`Page-specific module error: #${mod.tag}/${
|
|
145
|
+
console.error(`Page-specific module error: #${mod.tag}/${modPathLabel}`, e);
|
|
120
146
|
break; // Stop execution if any module fails
|
|
121
147
|
}
|
|
122
148
|
}
|
package/dist/test/runner.js
CHANGED
|
@@ -50,6 +50,10 @@ async function run(options = {}) {
|
|
|
50
50
|
console.log('');
|
|
51
51
|
console.log(chalk.bold(' Ultimate Jekyll Manager Tests'));
|
|
52
52
|
|
|
53
|
+
// Run the optional test/_init.js setup() hooks (framework + consumer) ONCE,
|
|
54
|
+
// before any suite. There is no cleanup hook — tests clean up after themselves.
|
|
55
|
+
await runInitSetups();
|
|
56
|
+
|
|
53
57
|
const results = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
54
58
|
|
|
55
59
|
if (sources.framework.length > 0) {
|
|
@@ -399,4 +403,62 @@ function relativizePath(file, source) {
|
|
|
399
403
|
return path.relative(path.join(process.cwd(), 'test'), file);
|
|
400
404
|
}
|
|
401
405
|
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// test/_init.js — pre-test lifecycle hook (setup only)
|
|
408
|
+
//
|
|
409
|
+
// Mirrors the backend framework's hook so all four frameworks share one shape.
|
|
410
|
+
// A project may add `<cwd>/test/_init.js` exporting a FUNCTION —
|
|
411
|
+
// `module.exports = (ctx) => ({ setup })` — called with `{ projectRoot }` and
|
|
412
|
+
// returning an object with an async `setup({ projectRoot })` that runs ONCE
|
|
413
|
+
// before any suite (e.g. to scaffold a fixture file the boot layer needs).
|
|
414
|
+
// There is no `cleanup` hook: tests clean up after themselves. Unlike the
|
|
415
|
+
// backend framework, there is no `accounts` field here — these frameworks have
|
|
416
|
+
// no auth/user system.
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
function loadInit(testDir, label) {
|
|
420
|
+
const initPath = path.join(testDir, '_init.js');
|
|
421
|
+
|
|
422
|
+
if (!jetpack.exists(initPath)) {
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const fn = require(initPath);
|
|
428
|
+
|
|
429
|
+
if (typeof fn !== 'function') {
|
|
430
|
+
console.log(chalk.red(` ✗ ${label} test/_init.js must export a function: module.exports = (ctx) => ({ ... })`));
|
|
431
|
+
return {};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const mod = fn({ projectRoot: process.cwd() });
|
|
435
|
+
return mod && typeof mod === 'object' ? mod : {};
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.log(chalk.red(` ✗ Failed to load ${label} test/_init.js: ${e.message}`));
|
|
438
|
+
return {};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function runInitSetups() {
|
|
443
|
+
const frameworkTestsDir = path.resolve(__dirname, '../../test');
|
|
444
|
+
const projectTestsDir = path.join(process.cwd(), 'test');
|
|
445
|
+
|
|
446
|
+
const hooks = [
|
|
447
|
+
loadInit(frameworkTestsDir, 'framework'),
|
|
448
|
+
loadInit(projectTestsDir, 'project'),
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
const setups = hooks.filter((h) => typeof h.setup === 'function').map((h) => h.setup);
|
|
452
|
+
|
|
453
|
+
for (const setup of setups) {
|
|
454
|
+
process.stdout.write(chalk.gray(' Running test/_init.js setup... '));
|
|
455
|
+
try {
|
|
456
|
+
await setup({ projectRoot: process.cwd() });
|
|
457
|
+
console.log(chalk.green('✓'));
|
|
458
|
+
} catch (e) {
|
|
459
|
+
console.log(chalk.red(`✗ (${e.message})`));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
402
464
|
module.exports = { run, SkipError };
|
|
@@ -86,18 +86,25 @@ module.exports = {
|
|
|
86
86
|
},
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
|
-
name: 'getEnvironment maps
|
|
89
|
+
name: 'getEnvironment maps to development/testing/production',
|
|
90
90
|
run: async (ctx) => {
|
|
91
91
|
const Manager = require('../../../build.js');
|
|
92
|
-
const
|
|
92
|
+
const origServer = process.env.UJ_IS_SERVER;
|
|
93
|
+
const origTest = process.env.UJ_TEST_MODE;
|
|
93
94
|
try {
|
|
95
|
+
// Testing wins over everything.
|
|
96
|
+
process.env.UJ_TEST_MODE = 'true';
|
|
97
|
+
process.env.UJ_IS_SERVER = 'true';
|
|
98
|
+
ctx.expect(Manager.getEnvironment()).toBe('testing');
|
|
99
|
+
// With testing cleared, server flag → production; absent → development.
|
|
100
|
+
delete process.env.UJ_TEST_MODE;
|
|
94
101
|
process.env.UJ_IS_SERVER = 'true';
|
|
95
102
|
ctx.expect(Manager.getEnvironment()).toBe('production');
|
|
96
103
|
delete process.env.UJ_IS_SERVER;
|
|
97
104
|
ctx.expect(Manager.getEnvironment()).toBe('development');
|
|
98
105
|
} finally {
|
|
99
|
-
if (
|
|
100
|
-
else process.env.
|
|
106
|
+
if (origServer === undefined) delete process.env.UJ_IS_SERVER; else process.env.UJ_IS_SERVER = origServer;
|
|
107
|
+
if (origTest === undefined) delete process.env.UJ_TEST_MODE; else process.env.UJ_TEST_MODE = origTest;
|
|
101
108
|
}
|
|
102
109
|
},
|
|
103
110
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
28
|
-
if (process.env.
|
|
29
|
-
if (process.env.
|
|
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
|
-
|
|
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
|
|
34
|
-
if (window.Configuration.uj.environment === 'production') return
|
|
46
|
+
if (window.Configuration.uj.environment === 'development') return 'development';
|
|
47
|
+
if (window.Configuration.uj.environment === 'production') return 'production';
|
|
35
48
|
}
|
|
36
|
-
|
|
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
|
|
66
|
+
return getEnvironment() === 'production';
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
function isTesting() {
|
|
44
|
-
|
|
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.
|
|
69
|
-
Manager.prototype.
|
|
70
|
-
Manager.prototype.
|
|
71
|
-
Manager.prototype.
|
|
72
|
-
Manager.
|
|
73
|
-
Manager.
|
|
74
|
-
Manager.
|
|
75
|
-
Manager.
|
|
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
|
|
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.
|