ultimate-jekyll-manager 1.1.9 → 1.2.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 +40 -0
- package/CLAUDE.md +138 -1775
- package/README.md +49 -20
- package/dist/build.js +3 -0
- package/dist/cli.js +1 -0
- package/dist/commands/test.js +56 -0
- package/dist/defaults/CLAUDE.md +72 -6
- package/dist/gulp/tasks/defaults.js +20 -4
- package/dist/gulp/tasks/distribute.js +29 -26
- package/dist/gulp/tasks/jsonToHtml.js +86 -80
- package/dist/gulp/tasks/minifyHtml.js +55 -51
- package/dist/gulp/tasks/sass.js +7 -6
- package/dist/gulp/tasks/utils/template-transform.js +35 -35
- package/dist/index.js +4 -0
- package/dist/test/assert.js +120 -0
- package/dist/test/fixtures/consumer-site/_site/about.html +13 -0
- package/dist/test/fixtures/consumer-site/_site/assets/css/main.bundle.css +2 -0
- package/dist/test/fixtures/consumer-site/_site/assets/js/main.bundle.js +6 -0
- package/dist/test/fixtures/consumer-site/_site/build.json +11 -0
- package/dist/test/fixtures/consumer-site/_site/index.html +28 -0
- package/dist/test/fixtures/consumer-site/_site/service-worker.js +29 -0
- package/dist/test/fixtures/consumer-site/package.json +6 -0
- package/dist/test/harness/page/index.html +51 -0
- package/dist/test/index.js +63 -0
- package/dist/test/runner.js +402 -0
- package/dist/test/runners/boot.js +109 -0
- package/dist/test/runners/chromium.js +255 -0
- package/dist/test/server.js +127 -0
- package/dist/test/suites/boot/service-worker.test.js +84 -0
- package/dist/test/suites/boot/site-loads.test.js +65 -0
- package/dist/test/suites/build/cli.test.js +49 -0
- package/dist/test/suites/build/collect-text-nodes.test.js +37 -0
- package/dist/test/suites/build/dictionary.test.js +17 -0
- package/dist/test/suites/build/expect.test.js +59 -0
- package/dist/test/suites/build/exports.test.js +32 -0
- package/dist/test/suites/build/logger.test.js +62 -0
- package/dist/test/suites/build/manager.test.js +186 -0
- package/dist/test/suites/build/merge-jekyll-configs.test.js +95 -0
- package/dist/test/suites/build/mode-helpers.test.js +65 -0
- package/dist/test/suites/build/template-transform.test.js +94 -0
- package/dist/test/suites/build/templating-brackets.test.js +46 -0
- package/dist/test/suites/build/validate-yaml.test.js +60 -0
- package/dist/test/suites/page/dom-baseline.test.js +34 -0
- package/dist/test/suites/page/harness-globals.test.js +38 -0
- package/dist/test/suites/page/prerendered-icons.test.js +32 -0
- package/dist/utils/mode-helpers.js +84 -0
- package/docs/_legacy-claude-md.md +1832 -0
- package/docs/cross-context-helpers.md +75 -0
- package/docs/test-boot-layer.md +110 -0
- package/docs/test-framework.md +183 -0
- package/package.json +18 -16
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Smoke-test the expect() implementation itself. Mirrors EM/BXM's expect.test.js.
|
|
2
|
+
// If this breaks, every other test reports nonsense — verify the matchers
|
|
3
|
+
// directly so the framework's own foundation can't silently rot.
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
layer: 'build',
|
|
7
|
+
description: 'expect() matcher set',
|
|
8
|
+
type: 'group',
|
|
9
|
+
tests: [
|
|
10
|
+
{
|
|
11
|
+
name: 'toBe + toEqual basics',
|
|
12
|
+
run: async (ctx) => {
|
|
13
|
+
ctx.expect(1).toBe(1);
|
|
14
|
+
ctx.expect({ a: 1 }).toEqual({ a: 1 });
|
|
15
|
+
ctx.expect([1, 2]).toEqual([1, 2]);
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: '.not negates',
|
|
20
|
+
run: async (ctx) => {
|
|
21
|
+
ctx.expect(1).not.toBe(2);
|
|
22
|
+
ctx.expect({ a: 1 }).not.toEqual({ a: 2 });
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'toContain works on arrays and strings',
|
|
27
|
+
run: async (ctx) => {
|
|
28
|
+
ctx.expect([1, 2, 3]).toContain(2);
|
|
29
|
+
ctx.expect('hello world').toContain('world');
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'toThrow catches sync + async throws',
|
|
34
|
+
run: async (ctx) => {
|
|
35
|
+
await ctx.expect(() => { throw new Error('boom'); }).toThrow('boom');
|
|
36
|
+
await ctx.expect(async () => { throw new Error('async boom'); }).toThrow(/async/);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'toBeGreaterThan / toBeLessThan',
|
|
41
|
+
run: async (ctx) => {
|
|
42
|
+
ctx.expect(5).toBeGreaterThan(3);
|
|
43
|
+
ctx.expect(3).toBeLessThan(5);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'failing assertions throw AssertionError',
|
|
48
|
+
run: async (ctx) => {
|
|
49
|
+
try {
|
|
50
|
+
ctx.expect(1).toBe(2);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
ctx.expect(e.name).toBe('AssertionError');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw new Error('expected assertion to throw');
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Verify every entry in package.json#exports is require()-able from the built
|
|
2
|
+
// dist/. Catches packaging mistakes where dist/ is missing a file referenced
|
|
3
|
+
// from exports (which would explode at consumer install-time).
|
|
4
|
+
//
|
|
5
|
+
// We resolve relative to dist/test/runner.js — same context the framework
|
|
6
|
+
// runs in.
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
layer: 'build',
|
|
10
|
+
description: 'package.json exports resolve to real files in dist/',
|
|
11
|
+
run: async (ctx) => {
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
// distRoot is the directory containing this very test file's grandparent
|
|
15
|
+
// → dist/test/suites/build/exports.test.js → ../../../ = dist/.
|
|
16
|
+
const distRoot = path.resolve(__dirname, '..', '..', '..');
|
|
17
|
+
const pkgPath = path.resolve(distRoot, '..', 'package.json');
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
19
|
+
|
|
20
|
+
ctx.expect(typeof pkg.exports).toBe('object');
|
|
21
|
+
|
|
22
|
+
for (const [subpath, target] of Object.entries(pkg.exports)) {
|
|
23
|
+
// target is './dist/<...>.js' — verify the file exists.
|
|
24
|
+
const targetPath = path.resolve(path.dirname(pkgPath), target);
|
|
25
|
+
ctx.expect(fs.existsSync(targetPath)).toBe(true);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Spot-check the canonical entry — require() it and verify shape.
|
|
29
|
+
const Manager = require('../../../build.js');
|
|
30
|
+
ctx.expect(typeof Manager).toBe('function');
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/lib/logger.js — the only file under src/lib/ currently. Used everywhere
|
|
2
|
+
// in UJM's gulp pipeline as `Manager.logger('task-name')`. Output shape matters
|
|
3
|
+
// because dev.log parsers (and humans grepping logs) depend on the `[time]
|
|
4
|
+
// name: message` prefix.
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
layer: 'build',
|
|
8
|
+
description: 'Logger (src/lib/logger.js)',
|
|
9
|
+
type: 'group',
|
|
10
|
+
tests: [
|
|
11
|
+
{
|
|
12
|
+
name: 'Logger constructor stores name',
|
|
13
|
+
run: async (ctx) => {
|
|
14
|
+
const Logger = require('../../../lib/logger.js');
|
|
15
|
+
const log = new Logger('my-task');
|
|
16
|
+
ctx.expect(log.name).toBe('my-task');
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Logger exposes log/error/warn/info methods',
|
|
21
|
+
run: async (ctx) => {
|
|
22
|
+
const Logger = require('../../../lib/logger.js');
|
|
23
|
+
const log = new Logger('x');
|
|
24
|
+
for (const m of ['log', 'error', 'warn', 'info']) {
|
|
25
|
+
ctx.expect(typeof log[m]).toBe('function');
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Logger.format is chalk',
|
|
31
|
+
run: async (ctx) => {
|
|
32
|
+
const Logger = require('../../../lib/logger.js');
|
|
33
|
+
const log = new Logger('x');
|
|
34
|
+
// chalk-with-default-export exposes `red`, `green`, `cyan` etc. as functions.
|
|
35
|
+
ctx.expect(typeof log.format.red).toBe('function');
|
|
36
|
+
ctx.expect(typeof log.format.green).toBe('function');
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Logger output goes through console with prefix',
|
|
41
|
+
run: async (ctx) => {
|
|
42
|
+
const Logger = require('../../../lib/logger.js');
|
|
43
|
+
const log = new Logger('prefix-test');
|
|
44
|
+
|
|
45
|
+
const captured = [];
|
|
46
|
+
const orig = console.log;
|
|
47
|
+
console.log = (...args) => captured.push(args);
|
|
48
|
+
try {
|
|
49
|
+
log.log('hello world');
|
|
50
|
+
} finally {
|
|
51
|
+
console.log = orig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ctx.expect(captured.length).toBe(1);
|
|
55
|
+
const first = captured[0][0];
|
|
56
|
+
// Strip ANSI for stable assertion.
|
|
57
|
+
const plain = first.replace(/\[[0-9;]*m/g, '');
|
|
58
|
+
ctx.expect(plain).toMatch(/\[\d\d:\d\d:\d\d\] 'prefix-test':/);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Manager static/prototype methods exposed by src/build.js.
|
|
2
|
+
//
|
|
3
|
+
// These are the public surface consumer projects consume through
|
|
4
|
+
// `require('ultimate-jekyll-manager/build')`. Tests assert that:
|
|
5
|
+
// - Static + instance forms match (Manager.foo === Manager.prototype.foo)
|
|
6
|
+
// - Env-gated boolean checks reflect process.env correctly
|
|
7
|
+
// - getArguments + getMemoryUsage return well-shaped objects
|
|
8
|
+
// - getRootPath/getEnvironment short-circuit checks don't throw
|
|
9
|
+
//
|
|
10
|
+
// We DO NOT assert getConfig/getPackage/getUJMConfig here because those
|
|
11
|
+
// require a Jekyll project on disk; consumer tests verify those against
|
|
12
|
+
// their own project.
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
layer: 'build',
|
|
16
|
+
description: 'Manager (build.js) public surface',
|
|
17
|
+
type: 'group',
|
|
18
|
+
tests: [
|
|
19
|
+
{
|
|
20
|
+
name: 'Manager constructor is a function',
|
|
21
|
+
run: async (ctx) => {
|
|
22
|
+
const Manager = require('../../../build.js');
|
|
23
|
+
ctx.expect(typeof Manager).toBe('function');
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'static methods match prototype methods',
|
|
28
|
+
run: async (ctx) => {
|
|
29
|
+
const Manager = require('../../../build.js');
|
|
30
|
+
const names = ['getArguments', 'isServer', 'isBuildMode', 'isQuickMode', 'actLikeProduction', 'getEnvironment', 'getRootPath', 'getMemoryUsage', 'logger'];
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
ctx.expect(typeof Manager[name]).toBe('function');
|
|
33
|
+
ctx.expect(typeof Manager.prototype[name]).toBe('function');
|
|
34
|
+
ctx.expect(Manager[name]).toBe(Manager.prototype[name]);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'isBuildMode reflects UJ_BUILD_MODE env',
|
|
40
|
+
run: async (ctx) => {
|
|
41
|
+
const Manager = require('../../../build.js');
|
|
42
|
+
const original = process.env.UJ_BUILD_MODE;
|
|
43
|
+
try {
|
|
44
|
+
process.env.UJ_BUILD_MODE = 'true';
|
|
45
|
+
ctx.expect(Manager.isBuildMode()).toBe(true);
|
|
46
|
+
process.env.UJ_BUILD_MODE = 'false';
|
|
47
|
+
ctx.expect(Manager.isBuildMode()).toBe(false);
|
|
48
|
+
delete process.env.UJ_BUILD_MODE;
|
|
49
|
+
ctx.expect(Manager.isBuildMode()).toBe(false);
|
|
50
|
+
} finally {
|
|
51
|
+
if (original === undefined) delete process.env.UJ_BUILD_MODE;
|
|
52
|
+
else process.env.UJ_BUILD_MODE = original;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'isQuickMode reflects UJ_QUICK env',
|
|
58
|
+
run: async (ctx) => {
|
|
59
|
+
const Manager = require('../../../build.js');
|
|
60
|
+
const original = process.env.UJ_QUICK;
|
|
61
|
+
try {
|
|
62
|
+
process.env.UJ_QUICK = 'true';
|
|
63
|
+
ctx.expect(Manager.isQuickMode()).toBe(true);
|
|
64
|
+
delete process.env.UJ_QUICK;
|
|
65
|
+
ctx.expect(Manager.isQuickMode()).toBe(false);
|
|
66
|
+
} finally {
|
|
67
|
+
if (original === undefined) delete process.env.UJ_QUICK;
|
|
68
|
+
else process.env.UJ_QUICK = original;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'isServer reflects UJ_IS_SERVER env',
|
|
74
|
+
run: async (ctx) => {
|
|
75
|
+
const Manager = require('../../../build.js');
|
|
76
|
+
const original = process.env.UJ_IS_SERVER;
|
|
77
|
+
try {
|
|
78
|
+
process.env.UJ_IS_SERVER = 'true';
|
|
79
|
+
ctx.expect(Manager.isServer()).toBe(true);
|
|
80
|
+
delete process.env.UJ_IS_SERVER;
|
|
81
|
+
ctx.expect(Manager.isServer()).toBe(false);
|
|
82
|
+
} finally {
|
|
83
|
+
if (original === undefined) delete process.env.UJ_IS_SERVER;
|
|
84
|
+
else process.env.UJ_IS_SERVER = original;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'getEnvironment maps server flag to environment string',
|
|
90
|
+
run: async (ctx) => {
|
|
91
|
+
const Manager = require('../../../build.js');
|
|
92
|
+
const original = process.env.UJ_IS_SERVER;
|
|
93
|
+
try {
|
|
94
|
+
process.env.UJ_IS_SERVER = 'true';
|
|
95
|
+
ctx.expect(Manager.getEnvironment()).toBe('production');
|
|
96
|
+
delete process.env.UJ_IS_SERVER;
|
|
97
|
+
ctx.expect(Manager.getEnvironment()).toBe('development');
|
|
98
|
+
} finally {
|
|
99
|
+
if (original === undefined) delete process.env.UJ_IS_SERVER;
|
|
100
|
+
else process.env.UJ_IS_SERVER = original;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'actLikeProduction is true when isBuildMode OR UJ_AUDIT_FORCE',
|
|
106
|
+
run: async (ctx) => {
|
|
107
|
+
const Manager = require('../../../build.js');
|
|
108
|
+
const origBuild = process.env.UJ_BUILD_MODE;
|
|
109
|
+
const origAudit = process.env.UJ_AUDIT_FORCE;
|
|
110
|
+
try {
|
|
111
|
+
delete process.env.UJ_BUILD_MODE;
|
|
112
|
+
delete process.env.UJ_AUDIT_FORCE;
|
|
113
|
+
ctx.expect(Manager.actLikeProduction()).toBe(false);
|
|
114
|
+
|
|
115
|
+
process.env.UJ_BUILD_MODE = 'true';
|
|
116
|
+
ctx.expect(Manager.actLikeProduction()).toBe(true);
|
|
117
|
+
delete process.env.UJ_BUILD_MODE;
|
|
118
|
+
|
|
119
|
+
process.env.UJ_AUDIT_FORCE = 'true';
|
|
120
|
+
ctx.expect(Manager.actLikeProduction()).toBe(true);
|
|
121
|
+
} finally {
|
|
122
|
+
if (origBuild === undefined) delete process.env.UJ_BUILD_MODE; else process.env.UJ_BUILD_MODE = origBuild;
|
|
123
|
+
if (origAudit === undefined) delete process.env.UJ_AUDIT_FORCE; else process.env.UJ_AUDIT_FORCE = origAudit;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'getRootPath("package") points at UJM root',
|
|
129
|
+
run: async (ctx) => {
|
|
130
|
+
const Manager = require('../../../build.js');
|
|
131
|
+
const path = require('path');
|
|
132
|
+
const root = Manager.getRootPath('package');
|
|
133
|
+
ctx.expect(typeof root).toBe('string');
|
|
134
|
+
// Should resolve to the directory containing src/build.js's parent.
|
|
135
|
+
// dist/build.js → dist/ ; package root = parent.
|
|
136
|
+
const fs = require('fs');
|
|
137
|
+
ctx.expect(fs.existsSync(path.join(root, 'package.json'))).toBe(true);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'getMemoryUsage returns shape with MB-sized numbers',
|
|
142
|
+
run: async (ctx) => {
|
|
143
|
+
const Manager = require('../../../build.js');
|
|
144
|
+
const mem = Manager.getMemoryUsage();
|
|
145
|
+
ctx.expect(typeof mem).toBe('object');
|
|
146
|
+
for (const key of ['rss', 'heapTotal', 'heapUsed', 'external']) {
|
|
147
|
+
ctx.expect(typeof mem[key]).toBe('number');
|
|
148
|
+
ctx.expect(mem[key]).toBeGreaterThan(0);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'getArguments returns object with _ array + boolean defaults',
|
|
154
|
+
run: async (ctx) => {
|
|
155
|
+
const Manager = require('../../../build.js');
|
|
156
|
+
const args = Manager.getArguments();
|
|
157
|
+
ctx.expect(typeof args).toBe('object');
|
|
158
|
+
ctx.expect(Array.isArray(args._)).toBe(true);
|
|
159
|
+
ctx.expect(typeof args.browser).toBe('boolean');
|
|
160
|
+
ctx.expect(typeof args.debug).toBe('boolean');
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'logger returns object with log/error/warn/info methods',
|
|
165
|
+
run: async (ctx) => {
|
|
166
|
+
const Manager = require('../../../build.js');
|
|
167
|
+
const log = Manager.logger('test-logger');
|
|
168
|
+
ctx.expect(typeof log.log).toBe('function');
|
|
169
|
+
ctx.expect(typeof log.error).toBe('function');
|
|
170
|
+
ctx.expect(typeof log.warn).toBe('function');
|
|
171
|
+
ctx.expect(typeof log.info).toBe('function');
|
|
172
|
+
ctx.expect(log.name).toBe('test-logger');
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'processBatches processes items in chunks and returns flat results',
|
|
177
|
+
run: async (ctx) => {
|
|
178
|
+
const Manager = require('../../../build.js');
|
|
179
|
+
const log = { log: () => {} };
|
|
180
|
+
const items = [1, 2, 3, 4, 5, 6, 7];
|
|
181
|
+
const results = await Manager.processBatches(items, 3, async (n) => n * 2, log);
|
|
182
|
+
ctx.expect(results).toEqual([2, 4, 6, 8, 10, 12, 14]);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// mergeJekyllConfigs from src/gulp/tasks/utils/merge-jekyll-configs.js.
|
|
2
|
+
// Critical for Jekyll's --config chain: project's `collections` + `defaults`
|
|
3
|
+
// must coexist with UJM's, not silently override. Regression here means
|
|
4
|
+
// custom collections vanish at build time.
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
layer: 'build',
|
|
12
|
+
description: 'mergeJekyllConfigs (utils/merge-jekyll-configs.js)',
|
|
13
|
+
type: 'group',
|
|
14
|
+
tests: [
|
|
15
|
+
{
|
|
16
|
+
name: 'merges collections from both configs (project additions win)',
|
|
17
|
+
run: async (ctx) => {
|
|
18
|
+
const mergeJekyllConfigs = require('../../../gulp/tasks/utils/merge-jekyll-configs.js');
|
|
19
|
+
const yaml = require('js-yaml');
|
|
20
|
+
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ujm-merge-'));
|
|
22
|
+
const ujmPath = path.join(dir, 'ujm.yml');
|
|
23
|
+
const projPath = path.join(dir, 'proj.yml');
|
|
24
|
+
const outPath = path.join(dir, 'merged.yml');
|
|
25
|
+
|
|
26
|
+
fs.writeFileSync(ujmPath, 'collections:\n posts:\n output: true\n team:\n output: true\n', 'utf8');
|
|
27
|
+
fs.writeFileSync(projPath, 'collections:\n posts:\n output: false\n blog:\n output: true\n', 'utf8');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = mergeJekyllConfigs(ujmPath, projPath, outPath, { log: () => {} });
|
|
31
|
+
ctx.expect(result).toBe(outPath);
|
|
32
|
+
const merged = yaml.load(fs.readFileSync(outPath, 'utf8'));
|
|
33
|
+
ctx.expect(merged.collections.team.output).toBe(true); // UJM-only retained
|
|
34
|
+
ctx.expect(merged.collections.blog.output).toBe(true); // project addition
|
|
35
|
+
ctx.expect(merged.collections.posts.output).toBe(false); // project wins on conflict
|
|
36
|
+
} finally {
|
|
37
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'dedups defaults by scope key (project wins)',
|
|
43
|
+
run: async (ctx) => {
|
|
44
|
+
const mergeJekyllConfigs = require('../../../gulp/tasks/utils/merge-jekyll-configs.js');
|
|
45
|
+
const yaml = require('js-yaml');
|
|
46
|
+
|
|
47
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ujm-merge-'));
|
|
48
|
+
const ujmPath = path.join(dir, 'ujm.yml');
|
|
49
|
+
const projPath = path.join(dir, 'proj.yml');
|
|
50
|
+
const outPath = path.join(dir, 'merged.yml');
|
|
51
|
+
|
|
52
|
+
fs.writeFileSync(ujmPath, `defaults:
|
|
53
|
+
- scope: { path: "dist/pages", type: "draft" }
|
|
54
|
+
values: { layout: "ujm-layout" }
|
|
55
|
+
- scope: { path: "dist/blog", type: "posts" }
|
|
56
|
+
values: { layout: "blog" }
|
|
57
|
+
`, 'utf8');
|
|
58
|
+
fs.writeFileSync(projPath, `defaults:
|
|
59
|
+
- scope: { path: "dist/pages", type: "draft" }
|
|
60
|
+
values: { layout: "project-layout" }
|
|
61
|
+
`, 'utf8');
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
mergeJekyllConfigs(ujmPath, projPath, outPath, { log: () => {} });
|
|
65
|
+
const merged = yaml.load(fs.readFileSync(outPath, 'utf8'));
|
|
66
|
+
const draft = merged.defaults.find((d) => d.scope.path === 'dist/pages' && d.scope.type === 'draft');
|
|
67
|
+
const blog = merged.defaults.find((d) => d.scope.path === 'dist/blog');
|
|
68
|
+
ctx.expect(draft.values.layout).toBe('project-layout'); // project wins
|
|
69
|
+
ctx.expect(blog.values.layout).toBe('blog'); // UJM-only retained
|
|
70
|
+
} finally {
|
|
71
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'returns null when there is nothing to merge',
|
|
77
|
+
run: async (ctx) => {
|
|
78
|
+
const mergeJekyllConfigs = require('../../../gulp/tasks/utils/merge-jekyll-configs.js');
|
|
79
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ujm-merge-'));
|
|
80
|
+
const ujmPath = path.join(dir, 'ujm.yml');
|
|
81
|
+
const projPath = path.join(dir, 'proj.yml');
|
|
82
|
+
const outPath = path.join(dir, 'merged.yml');
|
|
83
|
+
fs.writeFileSync(ujmPath, '# no collections or defaults\n', 'utf8');
|
|
84
|
+
fs.writeFileSync(projPath, '# also empty\n', 'utf8');
|
|
85
|
+
try {
|
|
86
|
+
const result = mergeJekyllConfigs(ujmPath, projPath, outPath, { log: () => {} });
|
|
87
|
+
ctx.expect(result).toBeNull();
|
|
88
|
+
ctx.expect(fs.existsSync(outPath)).toBe(false);
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Cross-context mode helpers (src/utils/mode-helpers.js) attached to the
|
|
2
|
+
// build.js Manager via attachTo(). These mirror EM/BXM's mode-helpers and
|
|
3
|
+
// are the canonical signal for "what kind of process am I in?".
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
layer: 'build',
|
|
7
|
+
description: 'mode-helpers (isTesting / isDevelopment / isProduction / getVersion)',
|
|
8
|
+
type: 'group',
|
|
9
|
+
tests: [
|
|
10
|
+
{
|
|
11
|
+
name: 'helpers attach to Manager statically AND on prototype',
|
|
12
|
+
run: async (ctx) => {
|
|
13
|
+
const Manager = require('../../../build.js');
|
|
14
|
+
for (const name of ['isTesting', 'isDevelopment', 'isProduction', 'getVersion']) {
|
|
15
|
+
ctx.expect(typeof Manager[name]).toBe('function');
|
|
16
|
+
ctx.expect(typeof Manager.prototype[name]).toBe('function');
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'isTesting reflects UJ_TEST_MODE env',
|
|
22
|
+
run: async (ctx) => {
|
|
23
|
+
const Manager = require('../../../build.js');
|
|
24
|
+
// UJ_TEST_MODE=true is set by `npx mgr test` — these tests run under it.
|
|
25
|
+
ctx.expect(Manager.isTesting()).toBe(true);
|
|
26
|
+
|
|
27
|
+
const original = process.env.UJ_TEST_MODE;
|
|
28
|
+
try {
|
|
29
|
+
delete process.env.UJ_TEST_MODE;
|
|
30
|
+
ctx.expect(Manager.isTesting()).toBe(false);
|
|
31
|
+
} finally {
|
|
32
|
+
if (original === undefined) delete process.env.UJ_TEST_MODE;
|
|
33
|
+
else process.env.UJ_TEST_MODE = original;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'isDevelopment false when UJ_BUILD_MODE=true',
|
|
39
|
+
run: async (ctx) => {
|
|
40
|
+
const Manager = require('../../../build.js');
|
|
41
|
+
const original = process.env.UJ_BUILD_MODE;
|
|
42
|
+
try {
|
|
43
|
+
process.env.UJ_BUILD_MODE = 'true';
|
|
44
|
+
ctx.expect(Manager.isDevelopment()).toBe(false);
|
|
45
|
+
ctx.expect(Manager.isProduction()).toBe(true);
|
|
46
|
+
} finally {
|
|
47
|
+
if (original === undefined) delete process.env.UJ_BUILD_MODE;
|
|
48
|
+
else process.env.UJ_BUILD_MODE = original;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'getVersion returns a non-empty string when run from a package',
|
|
54
|
+
run: async (ctx) => {
|
|
55
|
+
const Manager = require('../../../build.js');
|
|
56
|
+
const v = Manager.getVersion();
|
|
57
|
+
// May be null if cwd has no package.json; but in our test runs cwd is UJM root, so set.
|
|
58
|
+
if (v !== null) {
|
|
59
|
+
ctx.expect(typeof v).toBe('string');
|
|
60
|
+
ctx.expect(v.length).toBeGreaterThan(0);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// createTemplateTransform from src/gulp/tasks/utils/template-transform.js.
|
|
2
|
+
// A gulp Transform stream that templatizes file contents using node-powertools'
|
|
3
|
+
// `[ ]` bracket syntax. Used by defaults.js and distribute.js theme fallback.
|
|
4
|
+
//
|
|
5
|
+
// We don't run it through gulp — we synthesize a fake vinyl-like file object
|
|
6
|
+
// (with .isDirectory(), .path, .contents, .relative) and push it through the
|
|
7
|
+
// stream directly.
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
layer: 'build',
|
|
11
|
+
description: 'createTemplateTransform (utils/template-transform.js)',
|
|
12
|
+
type: 'group',
|
|
13
|
+
tests: [
|
|
14
|
+
{
|
|
15
|
+
name: 'replaces [site.theme.id] with config value in .html files',
|
|
16
|
+
run: async (ctx) => {
|
|
17
|
+
const createTemplateTransform = require('../../../gulp/tasks/utils/template-transform.js');
|
|
18
|
+
const tx = createTemplateTransform({ site: { theme: { id: 'classy' } } });
|
|
19
|
+
|
|
20
|
+
const file = {
|
|
21
|
+
path: '/tmp/test.html',
|
|
22
|
+
relative: 'test.html',
|
|
23
|
+
contents: Buffer.from('themes/[site.theme.id]/file.html'),
|
|
24
|
+
isDirectory: () => false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
tx.write(file);
|
|
29
|
+
tx.end();
|
|
30
|
+
tx.on('data', (f) => {
|
|
31
|
+
try {
|
|
32
|
+
ctx.expect(f.contents.toString()).toBe('themes/classy/file.html');
|
|
33
|
+
resolve();
|
|
34
|
+
} catch (e) { reject(e); }
|
|
35
|
+
});
|
|
36
|
+
tx.on('error', reject);
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'leaves non-matching extensions untouched (e.g. .css)',
|
|
42
|
+
run: async (ctx) => {
|
|
43
|
+
const createTemplateTransform = require('../../../gulp/tasks/utils/template-transform.js');
|
|
44
|
+
const tx = createTemplateTransform({ site: { theme: { id: 'classy' } } });
|
|
45
|
+
|
|
46
|
+
const original = 'a { content: "[site.theme.id]"; }';
|
|
47
|
+
const file = {
|
|
48
|
+
path: '/tmp/test.css',
|
|
49
|
+
relative: 'test.css',
|
|
50
|
+
contents: Buffer.from(original),
|
|
51
|
+
isDirectory: () => false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
tx.write(file);
|
|
56
|
+
tx.end();
|
|
57
|
+
tx.on('data', (f) => {
|
|
58
|
+
try {
|
|
59
|
+
ctx.expect(f.contents.toString()).toBe(original);
|
|
60
|
+
resolve();
|
|
61
|
+
} catch (e) { reject(e); }
|
|
62
|
+
});
|
|
63
|
+
tx.on('error', reject);
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'passes directories through untouched',
|
|
69
|
+
run: async (ctx) => {
|
|
70
|
+
const createTemplateTransform = require('../../../gulp/tasks/utils/template-transform.js');
|
|
71
|
+
const tx = createTemplateTransform({ site: {} });
|
|
72
|
+
|
|
73
|
+
const file = {
|
|
74
|
+
path: '/tmp/dir',
|
|
75
|
+
relative: 'dir',
|
|
76
|
+
contents: null,
|
|
77
|
+
isDirectory: () => true,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await new Promise((resolve, reject) => {
|
|
81
|
+
tx.write(file);
|
|
82
|
+
tx.end();
|
|
83
|
+
tx.on('data', (f) => {
|
|
84
|
+
try {
|
|
85
|
+
ctx.expect(f.contents).toBeNull();
|
|
86
|
+
resolve();
|
|
87
|
+
} catch (e) { reject(e); }
|
|
88
|
+
});
|
|
89
|
+
tx.on('error', reject);
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// UJM templating uses node-powertools' `template()`. Two bracket configs are
|
|
2
|
+
// in active use:
|
|
3
|
+
// `{ x }` — default (used wherever defaults.js / others omit `brackets`)
|
|
4
|
+
// `[ x ]` — distribute.js theme fallback + template-transform.js
|
|
5
|
+
//
|
|
6
|
+
// This suite locks in those conventions so a node-powertools upgrade or an
|
|
7
|
+
// accidental reconfiguration would fail loudly here rather than silently
|
|
8
|
+
// breaking Jekyll output. (Liquid syntax `{{ }}` is handled by Jekyll itself,
|
|
9
|
+
// NOT node-powertools — those placeholders pass through node-powertools
|
|
10
|
+
// untouched.)
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
layer: 'build',
|
|
14
|
+
description: 'node-powertools templating brackets ({} and [])',
|
|
15
|
+
type: 'group',
|
|
16
|
+
tests: [
|
|
17
|
+
{
|
|
18
|
+
name: 'default { } brackets resolve nested keys',
|
|
19
|
+
run: async (ctx) => {
|
|
20
|
+
const { template } = require('node-powertools');
|
|
21
|
+
const out = template('hello {name.first} {name.last}', {
|
|
22
|
+
name: { first: 'Ian', last: 'Wiedenman' },
|
|
23
|
+
});
|
|
24
|
+
ctx.expect(out).toBe('hello Ian Wiedenman');
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: '[ ] brackets resolve nested keys when explicitly configured',
|
|
29
|
+
run: async (ctx) => {
|
|
30
|
+
const { template } = require('node-powertools');
|
|
31
|
+
const out = template('themes/[site.theme.id]/file', {
|
|
32
|
+
site: { theme: { id: 'classy' } },
|
|
33
|
+
}, { brackets: ['[', ']'] });
|
|
34
|
+
ctx.expect(out).toBe('themes/classy/file');
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: '[ ] brackets leave Jekyll {{ }} placeholders alone',
|
|
39
|
+
run: async (ctx) => {
|
|
40
|
+
const { template } = require('node-powertools');
|
|
41
|
+
const out = template('a [x] b {{ x }} c', { x: 'X' }, { brackets: ['[', ']'] });
|
|
42
|
+
ctx.expect(out).toBe('a X b {{ x }} c');
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|