slicejs-cli 3.3.0 → 3.4.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/LICENSE +21 -21
- package/client.js +664 -626
- package/commands/Print.js +167 -167
- package/commands/Validations.js +103 -103
- package/commands/build/build.js +40 -40
- package/commands/buildProduction/buildProduction.js +579 -579
- package/commands/bundle/bundle.js +235 -235
- package/commands/createComponent/VisualComponentTemplate.js +55 -55
- package/commands/createComponent/createComponent.js +126 -126
- package/commands/deleteComponent/deleteComponent.js +77 -77
- package/commands/doctor/doctor.js +369 -369
- package/commands/getComponent/getComponent.js +747 -747
- package/commands/init/init.js +265 -261
- package/commands/listComponents/listComponents.js +175 -175
- package/commands/startServer/startServer.js +264 -264
- package/commands/startServer/watchServer.js +79 -79
- package/commands/types/types.js +16 -9
- package/commands/utils/LocalCliDelegation.js +53 -53
- package/commands/utils/PathHelper.js +68 -68
- package/commands/utils/VersionChecker.js +167 -167
- package/commands/utils/bundling/BundleGenerator.js +2292 -2292
- package/commands/utils/bundling/DependencyAnalyzer.js +933 -933
- package/commands/utils/updateManager.js +453 -453
- package/package.json +46 -46
- package/post.js +66 -65
- package/tests/bundle-generator.test.js +708 -708
- package/tests/bundle-v2-register-output.test.js +470 -470
- package/tests/client-launcher-contract.test.js +211 -211
- package/tests/client-update-flow-contract.test.js +272 -272
- package/tests/dependency-analyzer.test.js +24 -24
- package/tests/local-cli-delegation.test.js +79 -79
- package/tests/update-manager-notifications.test.js +88 -88
- package/.github/workflows/docs-render-cicd.yml +0 -65
|
@@ -1,708 +1,708 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import BundleGenerator from '../commands/utils/bundling/BundleGenerator.js';
|
|
7
|
-
|
|
8
|
-
const createComponent = (name, deps = []) => ({
|
|
9
|
-
name,
|
|
10
|
-
category: 'Visual',
|
|
11
|
-
categoryType: 'Visual',
|
|
12
|
-
dependencies: new Set(deps),
|
|
13
|
-
size: 10,
|
|
14
|
-
path: `/tmp/${name}`
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('computeBundleIntegrity returns sha256 hash', () => {
|
|
18
|
-
// Arrange
|
|
19
|
-
const generator = new BundleGenerator(import.meta.url, { components: [], routes: [], metrics: {} });
|
|
20
|
-
const components = [createComponent('Button', ['Input']), createComponent('Input')];
|
|
21
|
-
|
|
22
|
-
// Act
|
|
23
|
-
const integrity = generator.computeBundleIntegrity(components, 'critical', null, 'critical', 'slice-bundle.critical.js');
|
|
24
|
-
|
|
25
|
-
// Assert
|
|
26
|
-
assert.match(integrity, /^sha256:[a-f0-9]{64}$/);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('computeBundleIntegrity changes with component dependencies', () => {
|
|
30
|
-
// Arrange
|
|
31
|
-
const generator = new BundleGenerator(import.meta.url, { components: [], routes: [], metrics: {} });
|
|
32
|
-
const baseComponents = [createComponent('Button', ['Input']), createComponent('Input')];
|
|
33
|
-
const changedComponents = [createComponent('Button', ['Input', 'Icon']), createComponent('Input'), createComponent('Icon')];
|
|
34
|
-
|
|
35
|
-
// Act
|
|
36
|
-
const baseIntegrity = generator.computeBundleIntegrity(baseComponents, 'critical', null, 'critical', 'slice-bundle.critical.js');
|
|
37
|
-
const changedIntegrity = generator.computeBundleIntegrity(changedComponents, 'critical', null, 'critical', 'slice-bundle.critical.js');
|
|
38
|
-
|
|
39
|
-
// Assert
|
|
40
|
-
assert.notEqual(baseIntegrity, changedIntegrity);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('generateBundleConfig outputs V2 manifest fields', () => {
|
|
44
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
45
|
-
components: [],
|
|
46
|
-
routes: [],
|
|
47
|
-
metrics: {
|
|
48
|
-
totalComponents: 0,
|
|
49
|
-
totalRoutes: 0,
|
|
50
|
-
sharedPercentage: 0,
|
|
51
|
-
totalSize: 0
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const config = generator.generateBundleConfig(null);
|
|
56
|
-
|
|
57
|
-
assert.equal(config.format, 'v2');
|
|
58
|
-
assert.ok(config.generated);
|
|
59
|
-
assert.ok(config.bundles);
|
|
60
|
-
assert.ok(['enabled', 'disabled'].includes(config.loadingPolicy));
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('loading policy is enabled when sliceConfig loading.enabled is true', () => {
|
|
64
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
65
|
-
components: [],
|
|
66
|
-
routes: [],
|
|
67
|
-
metrics: {
|
|
68
|
-
totalComponents: 0,
|
|
69
|
-
totalRoutes: 0,
|
|
70
|
-
sharedPercentage: 0,
|
|
71
|
-
totalSize: 0
|
|
72
|
-
},
|
|
73
|
-
sliceConfig: { loading: { enabled: true } }
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const config = generator.generateBundleConfig(null);
|
|
77
|
-
assert.equal(config.loadingPolicy, 'enabled');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('loading policy falls back to project sliceConfig when analysisData lacks sliceConfig', async () => {
|
|
81
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slice-bundle-test-'));
|
|
82
|
-
const srcDir = path.join(tempRoot, 'src');
|
|
83
|
-
const previousInitCwd = process.env.INIT_CWD;
|
|
84
|
-
|
|
85
|
-
await fs.ensureDir(srcDir);
|
|
86
|
-
await fs.writeJson(path.join(srcDir, 'sliceConfig.json'), {
|
|
87
|
-
loading: { enabled: true }
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
process.env.INIT_CWD = tempRoot;
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
94
|
-
components: [],
|
|
95
|
-
routes: [],
|
|
96
|
-
metrics: {
|
|
97
|
-
totalComponents: 0,
|
|
98
|
-
totalRoutes: 0,
|
|
99
|
-
sharedPercentage: 0,
|
|
100
|
-
totalSize: 0
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const config = generator.generateBundleConfig(null);
|
|
105
|
-
assert.equal(config.loadingPolicy, 'enabled');
|
|
106
|
-
} finally {
|
|
107
|
-
if (previousInitCwd === undefined) {
|
|
108
|
-
delete process.env.INIT_CWD;
|
|
109
|
-
} else {
|
|
110
|
-
process.env.INIT_CWD = previousInitCwd;
|
|
111
|
-
}
|
|
112
|
-
await fs.remove(tempRoot);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('loading enabled always includes Loading component in critical bundle', () => {
|
|
117
|
-
const loading = {
|
|
118
|
-
...createComponent('Loading'),
|
|
119
|
-
routes: new Set(),
|
|
120
|
-
size: 100000
|
|
121
|
-
};
|
|
122
|
-
const home = {
|
|
123
|
-
...createComponent('HomePage'),
|
|
124
|
-
routes: new Set(['/'])
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
128
|
-
components: [loading, home],
|
|
129
|
-
routes: [{ path: '/', component: 'HomePage' }],
|
|
130
|
-
metrics: {
|
|
131
|
-
totalComponents: 2,
|
|
132
|
-
totalRoutes: 1,
|
|
133
|
-
sharedPercentage: 0,
|
|
134
|
-
totalSize: 100010
|
|
135
|
-
},
|
|
136
|
-
sliceConfig: { loading: { enabled: true } }
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
generator.identifyCriticalComponents();
|
|
140
|
-
|
|
141
|
-
assert.ok(generator.bundles.critical.components.some((component) => component.name === 'Loading'));
|
|
142
|
-
assert.equal(generator.generateBundleConfig().loadingPolicy, 'enabled');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('shared-core is wired as dependency for affected route bundles', () => {
|
|
146
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
147
|
-
components: [],
|
|
148
|
-
routes: [],
|
|
149
|
-
metrics: {
|
|
150
|
-
totalComponents: 0,
|
|
151
|
-
totalRoutes: 0,
|
|
152
|
-
sharedPercentage: 0,
|
|
153
|
-
totalSize: 0
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
generator.config.minSharedUsage = 2;
|
|
158
|
-
|
|
159
|
-
generator.bundles.routes = {
|
|
160
|
-
alpha: {
|
|
161
|
-
path: '/alpha',
|
|
162
|
-
components: [createComponent('SharedWidget'), createComponent('AlphaOnly')],
|
|
163
|
-
size: 20,
|
|
164
|
-
file: 'slice-bundle.alpha.js'
|
|
165
|
-
},
|
|
166
|
-
beta: {
|
|
167
|
-
path: '/beta',
|
|
168
|
-
components: [createComponent('SharedWidget'), createComponent('BetaOnly')],
|
|
169
|
-
size: 20,
|
|
170
|
-
file: 'slice-bundle.beta.js'
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
generator.extractSharedComponents(new Set());
|
|
175
|
-
const config = generator.generateBundleConfig();
|
|
176
|
-
|
|
177
|
-
assert.ok(config.bundles.routes['shared-core']);
|
|
178
|
-
assert.deepEqual(config.bundles.routes.alpha.dependencies, ['critical', 'shared-core']);
|
|
179
|
-
assert.deepEqual(config.bundles.routes.beta.dependencies, ['critical', 'shared-core']);
|
|
180
|
-
assert.deepEqual(config.routeBundles['/alpha'], ['critical', 'shared-core', 'alpha']);
|
|
181
|
-
assert.deepEqual(config.routeBundles['/beta'], ['critical', 'shared-core', 'beta']);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('rebalance merge preserves and merges route path metadata deterministically', () => {
|
|
185
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
186
|
-
components: [],
|
|
187
|
-
routes: [],
|
|
188
|
-
metrics: {
|
|
189
|
-
totalComponents: 0,
|
|
190
|
-
totalRoutes: 0,
|
|
191
|
-
sharedPercentage: 0,
|
|
192
|
-
totalSize: 0
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const bundles = {
|
|
197
|
-
alpha: { path: '/alpha', components: [createComponent('Alpha')], size: 10, file: 'slice-bundle.alpha.js' },
|
|
198
|
-
beta: { paths: ['/beta', '/beta-alt'], components: [createComponent('Beta')], size: 10, file: 'slice-bundle.beta.js' },
|
|
199
|
-
gamma: { path: '/gamma', components: [createComponent('Gamma')], size: 10, file: 'slice-bundle.gamma.js' }
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
generator.rebalanceBundlesByBudget(bundles, { maxBundleSize: 99999, maxRequests: 2 });
|
|
203
|
-
|
|
204
|
-
assert.equal(Object.keys(bundles).length, 2);
|
|
205
|
-
assert.deepEqual(bundles.beta.paths, ['/beta', '/beta-alt', '/gamma']);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('stripImports preserves absolute imports from configured publicFolders', () => {
|
|
209
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
210
|
-
components: [],
|
|
211
|
-
routes: [],
|
|
212
|
-
metrics: {},
|
|
213
|
-
sliceConfig: {
|
|
214
|
-
publicFolders: ['/public', '/assets']
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const source = "import logo from '/public/logo.js';\nimport hero from '/assets/hero.js';\nclass Demo {}\n";
|
|
219
|
-
const cleaned = generator.stripImports(source);
|
|
220
|
-
|
|
221
|
-
assert.match(cleaned, /import\s+logo\s+from\s+'\/public\/logo\.js';/);
|
|
222
|
-
assert.match(cleaned, /import\s+hero\s+from\s+'\/assets\/hero\.js';/);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
test('stripImports removes relative imports', () => {
|
|
226
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
227
|
-
components: [],
|
|
228
|
-
routes: [],
|
|
229
|
-
metrics: {},
|
|
230
|
-
sliceConfig: {
|
|
231
|
-
publicFolders: ['/public']
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const source = "import localDep from './local.js';\nimport parentDep from '../parent.js';\nclass Demo {}\n";
|
|
236
|
-
const cleaned = generator.stripImports(source);
|
|
237
|
-
|
|
238
|
-
assert.doesNotMatch(cleaned, /\.\/local\.js/);
|
|
239
|
-
assert.doesNotMatch(cleaned, /\.\.\/parent\.js/);
|
|
240
|
-
assert.match(cleaned, /class Demo \{\}/);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('stripImports warns on bare imports', () => {
|
|
244
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
245
|
-
components: [],
|
|
246
|
-
routes: [],
|
|
247
|
-
metrics: {},
|
|
248
|
-
sliceConfig: {
|
|
249
|
-
publicFolders: ['/public']
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const warnings = [];
|
|
254
|
-
const originalWarn = console.warn;
|
|
255
|
-
console.warn = (...args) => warnings.push(args.join(' '));
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const source = "import { html } from 'lit';\nclass Demo {}\n";
|
|
259
|
-
const cleaned = generator.stripImports(source);
|
|
260
|
-
|
|
261
|
-
assert.doesNotMatch(cleaned, /from\s+'lit'/);
|
|
262
|
-
assert.ok(warnings.some((msg) => msg.includes('bare import') && msg.includes('lit')));
|
|
263
|
-
} finally {
|
|
264
|
-
console.warn = originalWarn;
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test('stripImports warns on absolute imports outside publicFolders', () => {
|
|
269
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
270
|
-
components: [],
|
|
271
|
-
routes: [],
|
|
272
|
-
metrics: {},
|
|
273
|
-
sliceConfig: {
|
|
274
|
-
publicFolders: ['/public']
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const warnings = [];
|
|
279
|
-
const originalWarn = console.warn;
|
|
280
|
-
console.warn = (...args) => warnings.push(args.join(' '));
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const source = "import secret from '/private/secret.js';\nclass Demo {}\n";
|
|
284
|
-
const cleaned = generator.stripImports(source);
|
|
285
|
-
|
|
286
|
-
assert.doesNotMatch(cleaned, /\/private\/secret\.js/);
|
|
287
|
-
assert.ok(warnings.some((msg) => msg.includes('outside publicFolders') && msg.includes('/private/secret.js')));
|
|
288
|
-
} finally {
|
|
289
|
-
console.warn = originalWarn;
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test('stripImports supports side-effect and multiline imports in fallback mode', () => {
|
|
294
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
295
|
-
components: [],
|
|
296
|
-
routes: [],
|
|
297
|
-
metrics: {},
|
|
298
|
-
sliceConfig: {
|
|
299
|
-
publicFolders: ['/public']
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
const warnings = [];
|
|
304
|
-
const originalWarn = console.warn;
|
|
305
|
-
console.warn = (...args) => warnings.push(args.join(' '));
|
|
306
|
-
|
|
307
|
-
const originalParse = generator.parseImportsFromCode;
|
|
308
|
-
generator.parseImportsFromCode = () => {
|
|
309
|
-
throw new Error('forced parser failure');
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
const source = [
|
|
314
|
-
"import '/public/effects.js';",
|
|
315
|
-
"import '/private/effects.js';",
|
|
316
|
-
"import {",
|
|
317
|
-
' html,',
|
|
318
|
-
' css',
|
|
319
|
-
"} from 'lit';",
|
|
320
|
-
'class Demo {}'
|
|
321
|
-
].join('\n');
|
|
322
|
-
const cleaned = generator.stripImports(source, { sourceContext: 'DemoComponent' });
|
|
323
|
-
|
|
324
|
-
assert.match(cleaned, /import '\/public\/effects\.js';/);
|
|
325
|
-
assert.doesNotMatch(cleaned, /\/private\/effects\.js/);
|
|
326
|
-
assert.doesNotMatch(cleaned, /from 'lit'/);
|
|
327
|
-
assert.ok(warnings.some((msg) => msg.includes('outside publicFolders') && msg.includes('/private/effects.js') && msg.includes('[DemoComponent]')));
|
|
328
|
-
assert.ok(warnings.some((msg) => msg.includes('bare import') && msg.includes('lit') && msg.includes('[DemoComponent]')));
|
|
329
|
-
} finally {
|
|
330
|
-
generator.parseImportsFromCode = originalParse;
|
|
331
|
-
console.warn = originalWarn;
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test('cleanJavaScript hoists allowed absolute imports and removes them from component code', () => {
|
|
336
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
337
|
-
components: [],
|
|
338
|
-
routes: [],
|
|
339
|
-
metrics: {},
|
|
340
|
-
sliceConfig: {
|
|
341
|
-
publicFolders: ['/public']
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const source = [
|
|
346
|
-
"import hero from '/public/hero.js';",
|
|
347
|
-
'class Demo extends HTMLElement {}',
|
|
348
|
-
'customElements.define("x-demo", Demo);'
|
|
349
|
-
].join('\n');
|
|
350
|
-
|
|
351
|
-
const result = generator.cleanJavaScript(source, 'Demo', 'DemoPath.js');
|
|
352
|
-
|
|
353
|
-
assert.doesNotMatch(result.code, /import hero from '\/public\/hero\.js';/);
|
|
354
|
-
assert.ok(result.hoistedImports.includes("import hero from '/public/hero.js';"));
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test('formatBundleFile emits hoisted imports for framework-compatible output', () => {
|
|
358
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
359
|
-
components: [],
|
|
360
|
-
routes: [],
|
|
361
|
-
metrics: {}
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const source = generator.formatBundleFile({
|
|
365
|
-
'Framework/Structural/Bootstrap': {
|
|
366
|
-
name: 'Bootstrap',
|
|
367
|
-
category: 'Framework',
|
|
368
|
-
categoryType: 'Structural',
|
|
369
|
-
componentDependencies: [],
|
|
370
|
-
externalDependencies: {},
|
|
371
|
-
hoistedImports: ["import boot from '/public/bootstrap.js';"],
|
|
372
|
-
js: 'class Bootstrap extends HTMLElement {}\nreturn Bootstrap;',
|
|
373
|
-
html: '',
|
|
374
|
-
css: '',
|
|
375
|
-
size: 10,
|
|
376
|
-
isFramework: true
|
|
377
|
-
}
|
|
378
|
-
}, {
|
|
379
|
-
type: 'framework',
|
|
380
|
-
generated: new Date().toISOString(),
|
|
381
|
-
strategy: 'hybrid',
|
|
382
|
-
componentCount: 1,
|
|
383
|
-
totalSize: 10
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
assert.match(source, /import boot from '\/public\/bootstrap\.js';/);
|
|
387
|
-
assert.match(source, /const SLICE_BUNDLE_DEPENDENCIES = \{\};/);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test('default dependency binding resolves transformed default key over named exports', () => {
|
|
391
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
392
|
-
components: [],
|
|
393
|
-
routes: [],
|
|
394
|
-
metrics: {}
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const externalDependencies = {
|
|
398
|
-
'App/purify.js': {
|
|
399
|
-
content: 'export default () => "DEFAULT"; export const purify = () => "NAMED";',
|
|
400
|
-
bindings: [{ type: 'default', importedName: 'default', localName: 'purify' }]
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
405
|
-
const bindingsSource = generator.buildDependencyBindings(externalDependencies);
|
|
406
|
-
const resolveBoundValue = new Function(
|
|
407
|
-
`${resolverSource}\n` +
|
|
408
|
-
'const SLICE_BUNDLE_DEPENDENCIES = {"App/purify.js": { purifyData: "DEFAULT", purify: "NAMED" }};\n' +
|
|
409
|
-
`${bindingsSource}\n` +
|
|
410
|
-
'return purify;'
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
assert.equal(resolveBoundValue(), 'DEFAULT');
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
const evaluateDefaultResolver = ({ dep, depName = 'App/dep.js', preferredKey = null, calls = 1 }) => {
|
|
417
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
418
|
-
components: [],
|
|
419
|
-
routes: [],
|
|
420
|
-
metrics: {}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
424
|
-
const resolve = new Function(
|
|
425
|
-
'__dep',
|
|
426
|
-
'__depName',
|
|
427
|
-
'__preferredKey',
|
|
428
|
-
'__calls',
|
|
429
|
-
`${resolverSource}\n` +
|
|
430
|
-
'const __capturedWarnings = [];' +
|
|
431
|
-
'const __originalWarn = console.warn;' +
|
|
432
|
-
'console.warn = (...args) => __capturedWarnings.push(args.join(" "));' +
|
|
433
|
-
'let __result;' +
|
|
434
|
-
'try {' +
|
|
435
|
-
' for (let __i = 0; __i < __calls; __i += 1) {' +
|
|
436
|
-
' __result = __sliceResolveDefaultExport(__dep, __depName, __preferredKey);' +
|
|
437
|
-
' }' +
|
|
438
|
-
'} finally {' +
|
|
439
|
-
' console.warn = __originalWarn;' +
|
|
440
|
-
'}' +
|
|
441
|
-
'return { result: __result, warnings: __capturedWarnings };'
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
return resolve(dep, depName, preferredKey, calls);
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
test('default resolver returns default when present', () => {
|
|
448
|
-
const { result, warnings } = evaluateDefaultResolver({
|
|
449
|
-
dep: { default: 'DEFAULT', alpha: 'ALPHA' }
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
assert.equal(result, 'DEFAULT');
|
|
453
|
-
assert.equal(warnings.length, 0);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test('default resolver preserves falsy default values', () => {
|
|
457
|
-
const falsyValues = [0, '', false, null];
|
|
458
|
-
|
|
459
|
-
for (const value of falsyValues) {
|
|
460
|
-
const { result, warnings } = evaluateDefaultResolver({ dep: { default: value, alt: 'fallback' } });
|
|
461
|
-
assert.equal(result, value);
|
|
462
|
-
assert.equal(warnings.length, 0);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
test('default resolver falls back to single non-default key', () => {
|
|
467
|
-
const { result, warnings } = evaluateDefaultResolver({ dep: { onlyKey: 42 } });
|
|
468
|
-
|
|
469
|
-
assert.equal(result, 42);
|
|
470
|
-
assert.equal(warnings.length, 0);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
test('default resolver respects preferred key hint when present', () => {
|
|
474
|
-
const { result, warnings } = evaluateDefaultResolver({
|
|
475
|
-
dep: { purifyData: 'PREFERRED', purify: 'OTHER' },
|
|
476
|
-
depName: 'App/purify.js',
|
|
477
|
-
preferredKey: 'purifyData'
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
assert.equal(result, 'PREFERRED');
|
|
481
|
-
assert.equal(warnings.length, 0);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
test('default resolver prefers known keys module/exports/purify when unambiguous', () => {
|
|
485
|
-
const moduleResult = evaluateDefaultResolver({ dep: { module: 'MODULE', alpha: 'A' } });
|
|
486
|
-
const exportsResult = evaluateDefaultResolver({ dep: { exports: 'EXPORTS', beta: 'B' } });
|
|
487
|
-
const purifyResult = evaluateDefaultResolver({ dep: { purify: 'PURIFY', gamma: 'C' } });
|
|
488
|
-
|
|
489
|
-
assert.equal(moduleResult.result, 'MODULE');
|
|
490
|
-
assert.equal(exportsResult.result, 'EXPORTS');
|
|
491
|
-
assert.equal(purifyResult.result, 'PURIFY');
|
|
492
|
-
assert.equal(moduleResult.warnings.length, 0);
|
|
493
|
-
assert.equal(exportsResult.warnings.length, 0);
|
|
494
|
-
assert.equal(purifyResult.warnings.length, 0);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
test('default resolver uses deterministic alphabetical fallback on ambiguous keys', () => {
|
|
498
|
-
const { result } = evaluateDefaultResolver({ dep: { zebra: 'Z', alpha: 'A', middle: 'M' } });
|
|
499
|
-
assert.equal(result, 'A');
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
test('default resolver source uses locale-independent comparator and named preferred keys constant', () => {
|
|
503
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
504
|
-
components: [],
|
|
505
|
-
routes: [],
|
|
506
|
-
metrics: {}
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
510
|
-
|
|
511
|
-
assert.match(resolverSource, /const __sliceDefaultExportPreferredKeys = \['module', 'exports', 'purify'\];/);
|
|
512
|
-
assert.match(resolverSource, /const __sliceDeterministicKeyCompare = \(a, b\) => \(a < b \? -1 : a > b \? 1 : 0\);/);
|
|
513
|
-
assert.doesNotMatch(resolverSource, /localeCompare/);
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
test('default resolver warning includes dependency path keys and chosen key', () => {
|
|
517
|
-
const { warnings } = evaluateDefaultResolver({
|
|
518
|
-
dep: { zebra: 'Z', alpha: 'A', middle: 'M' },
|
|
519
|
-
depName: 'App/ambiguous.js'
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
assert.equal(warnings.length, 1);
|
|
523
|
-
assert.match(warnings[0], /App\/ambiguous\.js/);
|
|
524
|
-
assert.match(warnings[0], /Falling back to "alpha"/);
|
|
525
|
-
assert.match(warnings[0], /Keys: alpha, middle, zebra/);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
test('default resolver deduplicates ambiguous warning per dependency in one evaluation', () => {
|
|
529
|
-
const { warnings } = evaluateDefaultResolver({
|
|
530
|
-
dep: { c: 3, a: 1, b: 2 },
|
|
531
|
-
depName: 'App/repeat-warning.js',
|
|
532
|
-
calls: 3
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
assert.equal(warnings.length, 1);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
test('default resolver evaluation restores console.warn when resolver throws', () => {
|
|
539
|
-
const originalWarn = console.warn;
|
|
540
|
-
|
|
541
|
-
assert.throws(() => {
|
|
542
|
-
evaluateDefaultResolver({
|
|
543
|
-
dep: new Proxy({}, {
|
|
544
|
-
ownKeys() {
|
|
545
|
-
throw new Error('kaboom');
|
|
546
|
-
}
|
|
547
|
-
})
|
|
548
|
-
});
|
|
549
|
-
}, /kaboom/);
|
|
550
|
-
|
|
551
|
-
assert.equal(console.warn, originalWarn);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
test('default resolver passes through non-object values and null/undefined', () => {
|
|
555
|
-
const fn = () => 'ok';
|
|
556
|
-
const functionResult = evaluateDefaultResolver({ dep: fn });
|
|
557
|
-
const stringResult = evaluateDefaultResolver({ dep: 'value' });
|
|
558
|
-
const numberResult = evaluateDefaultResolver({ dep: 7 });
|
|
559
|
-
const nullResult = evaluateDefaultResolver({ dep: null });
|
|
560
|
-
const undefinedResult = evaluateDefaultResolver({ dep: undefined });
|
|
561
|
-
|
|
562
|
-
assert.equal(functionResult.result, fn);
|
|
563
|
-
assert.equal(stringResult.result, 'value');
|
|
564
|
-
assert.equal(numberResult.result, 7);
|
|
565
|
-
assert.equal(nullResult.result, null);
|
|
566
|
-
assert.equal(undefinedResult.result, undefined);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test('analyzeDependencies resolves extensionless imports across js json and mjs', async () => {
|
|
570
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slice-deps-ext-'));
|
|
571
|
-
const componentDir = path.join(tempRoot, 'Component');
|
|
572
|
-
|
|
573
|
-
await fs.ensureDir(componentDir);
|
|
574
|
-
await fs.writeFile(path.join(componentDir, 'dep-js.js'), 'export const value = 1;', 'utf-8');
|
|
575
|
-
await fs.writeFile(path.join(componentDir, 'dep-json.json'), '{"ok":true}', 'utf-8');
|
|
576
|
-
await fs.writeFile(path.join(componentDir, 'dep-mjs.mjs'), 'export const ok = true;', 'utf-8');
|
|
577
|
-
|
|
578
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
579
|
-
components: [],
|
|
580
|
-
routes: [],
|
|
581
|
-
metrics: {}
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
try {
|
|
585
|
-
const jsDeps = generator.analyzeDependencies("import dep from './dep-js';", componentDir);
|
|
586
|
-
const jsonDeps = generator.analyzeDependencies("import cfg from './dep-json';", componentDir);
|
|
587
|
-
const mjsDeps = generator.analyzeDependencies("import mod from './dep-mjs';", componentDir);
|
|
588
|
-
|
|
589
|
-
assert.equal(jsDeps.length, 1);
|
|
590
|
-
assert.equal(path.basename(jsDeps[0].path), 'dep-js.js');
|
|
591
|
-
assert.equal(jsonDeps.length, 1);
|
|
592
|
-
assert.equal(path.basename(jsonDeps[0].path), 'dep-json.json');
|
|
593
|
-
assert.equal(mjsDeps.length, 1);
|
|
594
|
-
assert.equal(path.basename(mjsDeps[0].path), 'dep-mjs.mjs');
|
|
595
|
-
} finally {
|
|
596
|
-
await fs.remove(tempRoot);
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
test('named and namespace dependency bindings remain unchanged', () => {
|
|
601
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
602
|
-
components: [],
|
|
603
|
-
routes: [],
|
|
604
|
-
metrics: {}
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
const externalDependencies = {
|
|
608
|
-
'App/named.js': {
|
|
609
|
-
content: 'export const alpha = 1;',
|
|
610
|
-
bindings: [
|
|
611
|
-
{ type: 'named', importedName: 'alpha', localName: 'localAlpha' },
|
|
612
|
-
{ type: 'namespace', localName: 'namedNamespace' }
|
|
613
|
-
]
|
|
614
|
-
}
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
618
|
-
const bindingsSource = generator.buildDependencyBindings(externalDependencies);
|
|
619
|
-
const evaluate = new Function(
|
|
620
|
-
`${resolverSource}\n` +
|
|
621
|
-
'const SLICE_BUNDLE_DEPENDENCIES = {"App/named.js": { alpha: 99, other: 1 }};' +
|
|
622
|
-
`${bindingsSource}\n` +
|
|
623
|
-
'return { localAlpha, namedNamespace };'
|
|
624
|
-
);
|
|
625
|
-
|
|
626
|
-
const values = evaluate();
|
|
627
|
-
assert.equal(values.localAlpha, 99);
|
|
628
|
-
assert.deepEqual(values.namedNamespace, { alpha: 99, other: 1 });
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
test('indexExternalDependencyUsage tracks unique route-bundle usage counts', () => {
|
|
632
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
633
|
-
components: [],
|
|
634
|
-
routes: [],
|
|
635
|
-
metrics: {}
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
const routeDependencyIndex = {
|
|
639
|
-
alpha: {
|
|
640
|
-
'deps/shared.js': { content: 'export const shared = true;' },
|
|
641
|
-
'deps/alpha.js': { content: 'export const alpha = true;' }
|
|
642
|
-
},
|
|
643
|
-
beta: {
|
|
644
|
-
'deps/shared.js': { content: 'export const shared = true;' },
|
|
645
|
-
'deps/beta.js': { content: 'export const beta = true;' }
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
650
|
-
|
|
651
|
-
assert.equal(usageIndex.get('deps/shared.js').bundleCount, 2);
|
|
652
|
-
assert.equal(usageIndex.get('deps/alpha.js').bundleCount, 1);
|
|
653
|
-
assert.deepEqual(Array.from(usageIndex.get('deps/shared.js').bundleKeys).sort(), ['alpha', 'beta']);
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
test('computeSharedDependencySet enforces usage and transformed-size thresholds', () => {
|
|
657
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
658
|
-
components: [],
|
|
659
|
-
routes: [],
|
|
660
|
-
metrics: {}
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
const largePayload = 'x'.repeat(2100);
|
|
664
|
-
const routeDependencyIndex = {
|
|
665
|
-
alpha: {
|
|
666
|
-
'deps/shared-large.js': { content: `export const payload = '${largePayload}';` },
|
|
667
|
-
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
668
|
-
},
|
|
669
|
-
beta: {
|
|
670
|
-
'deps/shared-large.js': { content: `export const payload = '${largePayload}';` },
|
|
671
|
-
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
672
|
-
},
|
|
673
|
-
gamma: {
|
|
674
|
-
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
679
|
-
const sharedSet = generator.computeSharedDependencySet(usageIndex);
|
|
680
|
-
|
|
681
|
-
assert.ok(sharedSet.has('deps/shared-large.js'));
|
|
682
|
-
assert.ok(!sharedSet.has('deps/shared-small.js'));
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
test('generateVendorSharedDependencyBundleContent emits shared dependency module once', () => {
|
|
686
|
-
const generator = new BundleGenerator(import.meta.url, {
|
|
687
|
-
components: [],
|
|
688
|
-
routes: [],
|
|
689
|
-
metrics: {}
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
const routeDependencyIndex = {
|
|
693
|
-
alpha: {
|
|
694
|
-
'deps/shared.js': { content: `export const payload = '${'y'.repeat(2200)}';` }
|
|
695
|
-
},
|
|
696
|
-
beta: {
|
|
697
|
-
'deps/shared.js': { content: `export const payload = '${'y'.repeat(2200)}';` }
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
702
|
-
const sharedSet = generator.computeSharedDependencySet(usageIndex);
|
|
703
|
-
generator.vendorShared.dependencyUsage = usageIndex;
|
|
704
|
-
const content = generator.generateVendorSharedDependencyBundleContent(sharedSet);
|
|
705
|
-
const assignmentMatches = content.match(/SLICE_BUNDLE_DEPENDENCIES\["deps\/shared\.js"\]/g) || [];
|
|
706
|
-
|
|
707
|
-
assert.equal(assignmentMatches.length, 1);
|
|
708
|
-
});
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import BundleGenerator from '../commands/utils/bundling/BundleGenerator.js';
|
|
7
|
+
|
|
8
|
+
const createComponent = (name, deps = []) => ({
|
|
9
|
+
name,
|
|
10
|
+
category: 'Visual',
|
|
11
|
+
categoryType: 'Visual',
|
|
12
|
+
dependencies: new Set(deps),
|
|
13
|
+
size: 10,
|
|
14
|
+
path: `/tmp/${name}`
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('computeBundleIntegrity returns sha256 hash', () => {
|
|
18
|
+
// Arrange
|
|
19
|
+
const generator = new BundleGenerator(import.meta.url, { components: [], routes: [], metrics: {} });
|
|
20
|
+
const components = [createComponent('Button', ['Input']), createComponent('Input')];
|
|
21
|
+
|
|
22
|
+
// Act
|
|
23
|
+
const integrity = generator.computeBundleIntegrity(components, 'critical', null, 'critical', 'slice-bundle.critical.js');
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
assert.match(integrity, /^sha256:[a-f0-9]{64}$/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('computeBundleIntegrity changes with component dependencies', () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const generator = new BundleGenerator(import.meta.url, { components: [], routes: [], metrics: {} });
|
|
32
|
+
const baseComponents = [createComponent('Button', ['Input']), createComponent('Input')];
|
|
33
|
+
const changedComponents = [createComponent('Button', ['Input', 'Icon']), createComponent('Input'), createComponent('Icon')];
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
const baseIntegrity = generator.computeBundleIntegrity(baseComponents, 'critical', null, 'critical', 'slice-bundle.critical.js');
|
|
37
|
+
const changedIntegrity = generator.computeBundleIntegrity(changedComponents, 'critical', null, 'critical', 'slice-bundle.critical.js');
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
assert.notEqual(baseIntegrity, changedIntegrity);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('generateBundleConfig outputs V2 manifest fields', () => {
|
|
44
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
45
|
+
components: [],
|
|
46
|
+
routes: [],
|
|
47
|
+
metrics: {
|
|
48
|
+
totalComponents: 0,
|
|
49
|
+
totalRoutes: 0,
|
|
50
|
+
sharedPercentage: 0,
|
|
51
|
+
totalSize: 0
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const config = generator.generateBundleConfig(null);
|
|
56
|
+
|
|
57
|
+
assert.equal(config.format, 'v2');
|
|
58
|
+
assert.ok(config.generated);
|
|
59
|
+
assert.ok(config.bundles);
|
|
60
|
+
assert.ok(['enabled', 'disabled'].includes(config.loadingPolicy));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('loading policy is enabled when sliceConfig loading.enabled is true', () => {
|
|
64
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
65
|
+
components: [],
|
|
66
|
+
routes: [],
|
|
67
|
+
metrics: {
|
|
68
|
+
totalComponents: 0,
|
|
69
|
+
totalRoutes: 0,
|
|
70
|
+
sharedPercentage: 0,
|
|
71
|
+
totalSize: 0
|
|
72
|
+
},
|
|
73
|
+
sliceConfig: { loading: { enabled: true } }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const config = generator.generateBundleConfig(null);
|
|
77
|
+
assert.equal(config.loadingPolicy, 'enabled');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('loading policy falls back to project sliceConfig when analysisData lacks sliceConfig', async () => {
|
|
81
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slice-bundle-test-'));
|
|
82
|
+
const srcDir = path.join(tempRoot, 'src');
|
|
83
|
+
const previousInitCwd = process.env.INIT_CWD;
|
|
84
|
+
|
|
85
|
+
await fs.ensureDir(srcDir);
|
|
86
|
+
await fs.writeJson(path.join(srcDir, 'sliceConfig.json'), {
|
|
87
|
+
loading: { enabled: true }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
process.env.INIT_CWD = tempRoot;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
94
|
+
components: [],
|
|
95
|
+
routes: [],
|
|
96
|
+
metrics: {
|
|
97
|
+
totalComponents: 0,
|
|
98
|
+
totalRoutes: 0,
|
|
99
|
+
sharedPercentage: 0,
|
|
100
|
+
totalSize: 0
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const config = generator.generateBundleConfig(null);
|
|
105
|
+
assert.equal(config.loadingPolicy, 'enabled');
|
|
106
|
+
} finally {
|
|
107
|
+
if (previousInitCwd === undefined) {
|
|
108
|
+
delete process.env.INIT_CWD;
|
|
109
|
+
} else {
|
|
110
|
+
process.env.INIT_CWD = previousInitCwd;
|
|
111
|
+
}
|
|
112
|
+
await fs.remove(tempRoot);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('loading enabled always includes Loading component in critical bundle', () => {
|
|
117
|
+
const loading = {
|
|
118
|
+
...createComponent('Loading'),
|
|
119
|
+
routes: new Set(),
|
|
120
|
+
size: 100000
|
|
121
|
+
};
|
|
122
|
+
const home = {
|
|
123
|
+
...createComponent('HomePage'),
|
|
124
|
+
routes: new Set(['/'])
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
128
|
+
components: [loading, home],
|
|
129
|
+
routes: [{ path: '/', component: 'HomePage' }],
|
|
130
|
+
metrics: {
|
|
131
|
+
totalComponents: 2,
|
|
132
|
+
totalRoutes: 1,
|
|
133
|
+
sharedPercentage: 0,
|
|
134
|
+
totalSize: 100010
|
|
135
|
+
},
|
|
136
|
+
sliceConfig: { loading: { enabled: true } }
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
generator.identifyCriticalComponents();
|
|
140
|
+
|
|
141
|
+
assert.ok(generator.bundles.critical.components.some((component) => component.name === 'Loading'));
|
|
142
|
+
assert.equal(generator.generateBundleConfig().loadingPolicy, 'enabled');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('shared-core is wired as dependency for affected route bundles', () => {
|
|
146
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
147
|
+
components: [],
|
|
148
|
+
routes: [],
|
|
149
|
+
metrics: {
|
|
150
|
+
totalComponents: 0,
|
|
151
|
+
totalRoutes: 0,
|
|
152
|
+
sharedPercentage: 0,
|
|
153
|
+
totalSize: 0
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
generator.config.minSharedUsage = 2;
|
|
158
|
+
|
|
159
|
+
generator.bundles.routes = {
|
|
160
|
+
alpha: {
|
|
161
|
+
path: '/alpha',
|
|
162
|
+
components: [createComponent('SharedWidget'), createComponent('AlphaOnly')],
|
|
163
|
+
size: 20,
|
|
164
|
+
file: 'slice-bundle.alpha.js'
|
|
165
|
+
},
|
|
166
|
+
beta: {
|
|
167
|
+
path: '/beta',
|
|
168
|
+
components: [createComponent('SharedWidget'), createComponent('BetaOnly')],
|
|
169
|
+
size: 20,
|
|
170
|
+
file: 'slice-bundle.beta.js'
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
generator.extractSharedComponents(new Set());
|
|
175
|
+
const config = generator.generateBundleConfig();
|
|
176
|
+
|
|
177
|
+
assert.ok(config.bundles.routes['shared-core']);
|
|
178
|
+
assert.deepEqual(config.bundles.routes.alpha.dependencies, ['critical', 'shared-core']);
|
|
179
|
+
assert.deepEqual(config.bundles.routes.beta.dependencies, ['critical', 'shared-core']);
|
|
180
|
+
assert.deepEqual(config.routeBundles['/alpha'], ['critical', 'shared-core', 'alpha']);
|
|
181
|
+
assert.deepEqual(config.routeBundles['/beta'], ['critical', 'shared-core', 'beta']);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('rebalance merge preserves and merges route path metadata deterministically', () => {
|
|
185
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
186
|
+
components: [],
|
|
187
|
+
routes: [],
|
|
188
|
+
metrics: {
|
|
189
|
+
totalComponents: 0,
|
|
190
|
+
totalRoutes: 0,
|
|
191
|
+
sharedPercentage: 0,
|
|
192
|
+
totalSize: 0
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const bundles = {
|
|
197
|
+
alpha: { path: '/alpha', components: [createComponent('Alpha')], size: 10, file: 'slice-bundle.alpha.js' },
|
|
198
|
+
beta: { paths: ['/beta', '/beta-alt'], components: [createComponent('Beta')], size: 10, file: 'slice-bundle.beta.js' },
|
|
199
|
+
gamma: { path: '/gamma', components: [createComponent('Gamma')], size: 10, file: 'slice-bundle.gamma.js' }
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
generator.rebalanceBundlesByBudget(bundles, { maxBundleSize: 99999, maxRequests: 2 });
|
|
203
|
+
|
|
204
|
+
assert.equal(Object.keys(bundles).length, 2);
|
|
205
|
+
assert.deepEqual(bundles.beta.paths, ['/beta', '/beta-alt', '/gamma']);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('stripImports preserves absolute imports from configured publicFolders', () => {
|
|
209
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
210
|
+
components: [],
|
|
211
|
+
routes: [],
|
|
212
|
+
metrics: {},
|
|
213
|
+
sliceConfig: {
|
|
214
|
+
publicFolders: ['/public', '/assets']
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const source = "import logo from '/public/logo.js';\nimport hero from '/assets/hero.js';\nclass Demo {}\n";
|
|
219
|
+
const cleaned = generator.stripImports(source);
|
|
220
|
+
|
|
221
|
+
assert.match(cleaned, /import\s+logo\s+from\s+'\/public\/logo\.js';/);
|
|
222
|
+
assert.match(cleaned, /import\s+hero\s+from\s+'\/assets\/hero\.js';/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('stripImports removes relative imports', () => {
|
|
226
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
227
|
+
components: [],
|
|
228
|
+
routes: [],
|
|
229
|
+
metrics: {},
|
|
230
|
+
sliceConfig: {
|
|
231
|
+
publicFolders: ['/public']
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const source = "import localDep from './local.js';\nimport parentDep from '../parent.js';\nclass Demo {}\n";
|
|
236
|
+
const cleaned = generator.stripImports(source);
|
|
237
|
+
|
|
238
|
+
assert.doesNotMatch(cleaned, /\.\/local\.js/);
|
|
239
|
+
assert.doesNotMatch(cleaned, /\.\.\/parent\.js/);
|
|
240
|
+
assert.match(cleaned, /class Demo \{\}/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('stripImports warns on bare imports', () => {
|
|
244
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
245
|
+
components: [],
|
|
246
|
+
routes: [],
|
|
247
|
+
metrics: {},
|
|
248
|
+
sliceConfig: {
|
|
249
|
+
publicFolders: ['/public']
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const warnings = [];
|
|
254
|
+
const originalWarn = console.warn;
|
|
255
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const source = "import { html } from 'lit';\nclass Demo {}\n";
|
|
259
|
+
const cleaned = generator.stripImports(source);
|
|
260
|
+
|
|
261
|
+
assert.doesNotMatch(cleaned, /from\s+'lit'/);
|
|
262
|
+
assert.ok(warnings.some((msg) => msg.includes('bare import') && msg.includes('lit')));
|
|
263
|
+
} finally {
|
|
264
|
+
console.warn = originalWarn;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('stripImports warns on absolute imports outside publicFolders', () => {
|
|
269
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
270
|
+
components: [],
|
|
271
|
+
routes: [],
|
|
272
|
+
metrics: {},
|
|
273
|
+
sliceConfig: {
|
|
274
|
+
publicFolders: ['/public']
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const warnings = [];
|
|
279
|
+
const originalWarn = console.warn;
|
|
280
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const source = "import secret from '/private/secret.js';\nclass Demo {}\n";
|
|
284
|
+
const cleaned = generator.stripImports(source);
|
|
285
|
+
|
|
286
|
+
assert.doesNotMatch(cleaned, /\/private\/secret\.js/);
|
|
287
|
+
assert.ok(warnings.some((msg) => msg.includes('outside publicFolders') && msg.includes('/private/secret.js')));
|
|
288
|
+
} finally {
|
|
289
|
+
console.warn = originalWarn;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('stripImports supports side-effect and multiline imports in fallback mode', () => {
|
|
294
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
295
|
+
components: [],
|
|
296
|
+
routes: [],
|
|
297
|
+
metrics: {},
|
|
298
|
+
sliceConfig: {
|
|
299
|
+
publicFolders: ['/public']
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const warnings = [];
|
|
304
|
+
const originalWarn = console.warn;
|
|
305
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
306
|
+
|
|
307
|
+
const originalParse = generator.parseImportsFromCode;
|
|
308
|
+
generator.parseImportsFromCode = () => {
|
|
309
|
+
throw new Error('forced parser failure');
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const source = [
|
|
314
|
+
"import '/public/effects.js';",
|
|
315
|
+
"import '/private/effects.js';",
|
|
316
|
+
"import {",
|
|
317
|
+
' html,',
|
|
318
|
+
' css',
|
|
319
|
+
"} from 'lit';",
|
|
320
|
+
'class Demo {}'
|
|
321
|
+
].join('\n');
|
|
322
|
+
const cleaned = generator.stripImports(source, { sourceContext: 'DemoComponent' });
|
|
323
|
+
|
|
324
|
+
assert.match(cleaned, /import '\/public\/effects\.js';/);
|
|
325
|
+
assert.doesNotMatch(cleaned, /\/private\/effects\.js/);
|
|
326
|
+
assert.doesNotMatch(cleaned, /from 'lit'/);
|
|
327
|
+
assert.ok(warnings.some((msg) => msg.includes('outside publicFolders') && msg.includes('/private/effects.js') && msg.includes('[DemoComponent]')));
|
|
328
|
+
assert.ok(warnings.some((msg) => msg.includes('bare import') && msg.includes('lit') && msg.includes('[DemoComponent]')));
|
|
329
|
+
} finally {
|
|
330
|
+
generator.parseImportsFromCode = originalParse;
|
|
331
|
+
console.warn = originalWarn;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('cleanJavaScript hoists allowed absolute imports and removes them from component code', () => {
|
|
336
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
337
|
+
components: [],
|
|
338
|
+
routes: [],
|
|
339
|
+
metrics: {},
|
|
340
|
+
sliceConfig: {
|
|
341
|
+
publicFolders: ['/public']
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const source = [
|
|
346
|
+
"import hero from '/public/hero.js';",
|
|
347
|
+
'class Demo extends HTMLElement {}',
|
|
348
|
+
'customElements.define("x-demo", Demo);'
|
|
349
|
+
].join('\n');
|
|
350
|
+
|
|
351
|
+
const result = generator.cleanJavaScript(source, 'Demo', 'DemoPath.js');
|
|
352
|
+
|
|
353
|
+
assert.doesNotMatch(result.code, /import hero from '\/public\/hero\.js';/);
|
|
354
|
+
assert.ok(result.hoistedImports.includes("import hero from '/public/hero.js';"));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('formatBundleFile emits hoisted imports for framework-compatible output', () => {
|
|
358
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
359
|
+
components: [],
|
|
360
|
+
routes: [],
|
|
361
|
+
metrics: {}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const source = generator.formatBundleFile({
|
|
365
|
+
'Framework/Structural/Bootstrap': {
|
|
366
|
+
name: 'Bootstrap',
|
|
367
|
+
category: 'Framework',
|
|
368
|
+
categoryType: 'Structural',
|
|
369
|
+
componentDependencies: [],
|
|
370
|
+
externalDependencies: {},
|
|
371
|
+
hoistedImports: ["import boot from '/public/bootstrap.js';"],
|
|
372
|
+
js: 'class Bootstrap extends HTMLElement {}\nreturn Bootstrap;',
|
|
373
|
+
html: '',
|
|
374
|
+
css: '',
|
|
375
|
+
size: 10,
|
|
376
|
+
isFramework: true
|
|
377
|
+
}
|
|
378
|
+
}, {
|
|
379
|
+
type: 'framework',
|
|
380
|
+
generated: new Date().toISOString(),
|
|
381
|
+
strategy: 'hybrid',
|
|
382
|
+
componentCount: 1,
|
|
383
|
+
totalSize: 10
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
assert.match(source, /import boot from '\/public\/bootstrap\.js';/);
|
|
387
|
+
assert.match(source, /const SLICE_BUNDLE_DEPENDENCIES = \{\};/);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('default dependency binding resolves transformed default key over named exports', () => {
|
|
391
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
392
|
+
components: [],
|
|
393
|
+
routes: [],
|
|
394
|
+
metrics: {}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const externalDependencies = {
|
|
398
|
+
'App/purify.js': {
|
|
399
|
+
content: 'export default () => "DEFAULT"; export const purify = () => "NAMED";',
|
|
400
|
+
bindings: [{ type: 'default', importedName: 'default', localName: 'purify' }]
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
405
|
+
const bindingsSource = generator.buildDependencyBindings(externalDependencies);
|
|
406
|
+
const resolveBoundValue = new Function(
|
|
407
|
+
`${resolverSource}\n` +
|
|
408
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {"App/purify.js": { purifyData: "DEFAULT", purify: "NAMED" }};\n' +
|
|
409
|
+
`${bindingsSource}\n` +
|
|
410
|
+
'return purify;'
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
assert.equal(resolveBoundValue(), 'DEFAULT');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const evaluateDefaultResolver = ({ dep, depName = 'App/dep.js', preferredKey = null, calls = 1 }) => {
|
|
417
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
418
|
+
components: [],
|
|
419
|
+
routes: [],
|
|
420
|
+
metrics: {}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
424
|
+
const resolve = new Function(
|
|
425
|
+
'__dep',
|
|
426
|
+
'__depName',
|
|
427
|
+
'__preferredKey',
|
|
428
|
+
'__calls',
|
|
429
|
+
`${resolverSource}\n` +
|
|
430
|
+
'const __capturedWarnings = [];' +
|
|
431
|
+
'const __originalWarn = console.warn;' +
|
|
432
|
+
'console.warn = (...args) => __capturedWarnings.push(args.join(" "));' +
|
|
433
|
+
'let __result;' +
|
|
434
|
+
'try {' +
|
|
435
|
+
' for (let __i = 0; __i < __calls; __i += 1) {' +
|
|
436
|
+
' __result = __sliceResolveDefaultExport(__dep, __depName, __preferredKey);' +
|
|
437
|
+
' }' +
|
|
438
|
+
'} finally {' +
|
|
439
|
+
' console.warn = __originalWarn;' +
|
|
440
|
+
'}' +
|
|
441
|
+
'return { result: __result, warnings: __capturedWarnings };'
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
return resolve(dep, depName, preferredKey, calls);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
test('default resolver returns default when present', () => {
|
|
448
|
+
const { result, warnings } = evaluateDefaultResolver({
|
|
449
|
+
dep: { default: 'DEFAULT', alpha: 'ALPHA' }
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
assert.equal(result, 'DEFAULT');
|
|
453
|
+
assert.equal(warnings.length, 0);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('default resolver preserves falsy default values', () => {
|
|
457
|
+
const falsyValues = [0, '', false, null];
|
|
458
|
+
|
|
459
|
+
for (const value of falsyValues) {
|
|
460
|
+
const { result, warnings } = evaluateDefaultResolver({ dep: { default: value, alt: 'fallback' } });
|
|
461
|
+
assert.equal(result, value);
|
|
462
|
+
assert.equal(warnings.length, 0);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('default resolver falls back to single non-default key', () => {
|
|
467
|
+
const { result, warnings } = evaluateDefaultResolver({ dep: { onlyKey: 42 } });
|
|
468
|
+
|
|
469
|
+
assert.equal(result, 42);
|
|
470
|
+
assert.equal(warnings.length, 0);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('default resolver respects preferred key hint when present', () => {
|
|
474
|
+
const { result, warnings } = evaluateDefaultResolver({
|
|
475
|
+
dep: { purifyData: 'PREFERRED', purify: 'OTHER' },
|
|
476
|
+
depName: 'App/purify.js',
|
|
477
|
+
preferredKey: 'purifyData'
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
assert.equal(result, 'PREFERRED');
|
|
481
|
+
assert.equal(warnings.length, 0);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('default resolver prefers known keys module/exports/purify when unambiguous', () => {
|
|
485
|
+
const moduleResult = evaluateDefaultResolver({ dep: { module: 'MODULE', alpha: 'A' } });
|
|
486
|
+
const exportsResult = evaluateDefaultResolver({ dep: { exports: 'EXPORTS', beta: 'B' } });
|
|
487
|
+
const purifyResult = evaluateDefaultResolver({ dep: { purify: 'PURIFY', gamma: 'C' } });
|
|
488
|
+
|
|
489
|
+
assert.equal(moduleResult.result, 'MODULE');
|
|
490
|
+
assert.equal(exportsResult.result, 'EXPORTS');
|
|
491
|
+
assert.equal(purifyResult.result, 'PURIFY');
|
|
492
|
+
assert.equal(moduleResult.warnings.length, 0);
|
|
493
|
+
assert.equal(exportsResult.warnings.length, 0);
|
|
494
|
+
assert.equal(purifyResult.warnings.length, 0);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('default resolver uses deterministic alphabetical fallback on ambiguous keys', () => {
|
|
498
|
+
const { result } = evaluateDefaultResolver({ dep: { zebra: 'Z', alpha: 'A', middle: 'M' } });
|
|
499
|
+
assert.equal(result, 'A');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('default resolver source uses locale-independent comparator and named preferred keys constant', () => {
|
|
503
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
504
|
+
components: [],
|
|
505
|
+
routes: [],
|
|
506
|
+
metrics: {}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
510
|
+
|
|
511
|
+
assert.match(resolverSource, /const __sliceDefaultExportPreferredKeys = \['module', 'exports', 'purify'\];/);
|
|
512
|
+
assert.match(resolverSource, /const __sliceDeterministicKeyCompare = \(a, b\) => \(a < b \? -1 : a > b \? 1 : 0\);/);
|
|
513
|
+
assert.doesNotMatch(resolverSource, /localeCompare/);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('default resolver warning includes dependency path keys and chosen key', () => {
|
|
517
|
+
const { warnings } = evaluateDefaultResolver({
|
|
518
|
+
dep: { zebra: 'Z', alpha: 'A', middle: 'M' },
|
|
519
|
+
depName: 'App/ambiguous.js'
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
assert.equal(warnings.length, 1);
|
|
523
|
+
assert.match(warnings[0], /App\/ambiguous\.js/);
|
|
524
|
+
assert.match(warnings[0], /Falling back to "alpha"/);
|
|
525
|
+
assert.match(warnings[0], /Keys: alpha, middle, zebra/);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('default resolver deduplicates ambiguous warning per dependency in one evaluation', () => {
|
|
529
|
+
const { warnings } = evaluateDefaultResolver({
|
|
530
|
+
dep: { c: 3, a: 1, b: 2 },
|
|
531
|
+
depName: 'App/repeat-warning.js',
|
|
532
|
+
calls: 3
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
assert.equal(warnings.length, 1);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('default resolver evaluation restores console.warn when resolver throws', () => {
|
|
539
|
+
const originalWarn = console.warn;
|
|
540
|
+
|
|
541
|
+
assert.throws(() => {
|
|
542
|
+
evaluateDefaultResolver({
|
|
543
|
+
dep: new Proxy({}, {
|
|
544
|
+
ownKeys() {
|
|
545
|
+
throw new Error('kaboom');
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
});
|
|
549
|
+
}, /kaboom/);
|
|
550
|
+
|
|
551
|
+
assert.equal(console.warn, originalWarn);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('default resolver passes through non-object values and null/undefined', () => {
|
|
555
|
+
const fn = () => 'ok';
|
|
556
|
+
const functionResult = evaluateDefaultResolver({ dep: fn });
|
|
557
|
+
const stringResult = evaluateDefaultResolver({ dep: 'value' });
|
|
558
|
+
const numberResult = evaluateDefaultResolver({ dep: 7 });
|
|
559
|
+
const nullResult = evaluateDefaultResolver({ dep: null });
|
|
560
|
+
const undefinedResult = evaluateDefaultResolver({ dep: undefined });
|
|
561
|
+
|
|
562
|
+
assert.equal(functionResult.result, fn);
|
|
563
|
+
assert.equal(stringResult.result, 'value');
|
|
564
|
+
assert.equal(numberResult.result, 7);
|
|
565
|
+
assert.equal(nullResult.result, null);
|
|
566
|
+
assert.equal(undefinedResult.result, undefined);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('analyzeDependencies resolves extensionless imports across js json and mjs', async () => {
|
|
570
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slice-deps-ext-'));
|
|
571
|
+
const componentDir = path.join(tempRoot, 'Component');
|
|
572
|
+
|
|
573
|
+
await fs.ensureDir(componentDir);
|
|
574
|
+
await fs.writeFile(path.join(componentDir, 'dep-js.js'), 'export const value = 1;', 'utf-8');
|
|
575
|
+
await fs.writeFile(path.join(componentDir, 'dep-json.json'), '{"ok":true}', 'utf-8');
|
|
576
|
+
await fs.writeFile(path.join(componentDir, 'dep-mjs.mjs'), 'export const ok = true;', 'utf-8');
|
|
577
|
+
|
|
578
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
579
|
+
components: [],
|
|
580
|
+
routes: [],
|
|
581
|
+
metrics: {}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const jsDeps = generator.analyzeDependencies("import dep from './dep-js';", componentDir);
|
|
586
|
+
const jsonDeps = generator.analyzeDependencies("import cfg from './dep-json';", componentDir);
|
|
587
|
+
const mjsDeps = generator.analyzeDependencies("import mod from './dep-mjs';", componentDir);
|
|
588
|
+
|
|
589
|
+
assert.equal(jsDeps.length, 1);
|
|
590
|
+
assert.equal(path.basename(jsDeps[0].path), 'dep-js.js');
|
|
591
|
+
assert.equal(jsonDeps.length, 1);
|
|
592
|
+
assert.equal(path.basename(jsonDeps[0].path), 'dep-json.json');
|
|
593
|
+
assert.equal(mjsDeps.length, 1);
|
|
594
|
+
assert.equal(path.basename(mjsDeps[0].path), 'dep-mjs.mjs');
|
|
595
|
+
} finally {
|
|
596
|
+
await fs.remove(tempRoot);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test('named and namespace dependency bindings remain unchanged', () => {
|
|
601
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
602
|
+
components: [],
|
|
603
|
+
routes: [],
|
|
604
|
+
metrics: {}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const externalDependencies = {
|
|
608
|
+
'App/named.js': {
|
|
609
|
+
content: 'export const alpha = 1;',
|
|
610
|
+
bindings: [
|
|
611
|
+
{ type: 'named', importedName: 'alpha', localName: 'localAlpha' },
|
|
612
|
+
{ type: 'namespace', localName: 'namedNamespace' }
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const resolverSource = generator.getDefaultExportResolverLines().join('\n');
|
|
618
|
+
const bindingsSource = generator.buildDependencyBindings(externalDependencies);
|
|
619
|
+
const evaluate = new Function(
|
|
620
|
+
`${resolverSource}\n` +
|
|
621
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {"App/named.js": { alpha: 99, other: 1 }};' +
|
|
622
|
+
`${bindingsSource}\n` +
|
|
623
|
+
'return { localAlpha, namedNamespace };'
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const values = evaluate();
|
|
627
|
+
assert.equal(values.localAlpha, 99);
|
|
628
|
+
assert.deepEqual(values.namedNamespace, { alpha: 99, other: 1 });
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test('indexExternalDependencyUsage tracks unique route-bundle usage counts', () => {
|
|
632
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
633
|
+
components: [],
|
|
634
|
+
routes: [],
|
|
635
|
+
metrics: {}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const routeDependencyIndex = {
|
|
639
|
+
alpha: {
|
|
640
|
+
'deps/shared.js': { content: 'export const shared = true;' },
|
|
641
|
+
'deps/alpha.js': { content: 'export const alpha = true;' }
|
|
642
|
+
},
|
|
643
|
+
beta: {
|
|
644
|
+
'deps/shared.js': { content: 'export const shared = true;' },
|
|
645
|
+
'deps/beta.js': { content: 'export const beta = true;' }
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
650
|
+
|
|
651
|
+
assert.equal(usageIndex.get('deps/shared.js').bundleCount, 2);
|
|
652
|
+
assert.equal(usageIndex.get('deps/alpha.js').bundleCount, 1);
|
|
653
|
+
assert.deepEqual(Array.from(usageIndex.get('deps/shared.js').bundleKeys).sort(), ['alpha', 'beta']);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test('computeSharedDependencySet enforces usage and transformed-size thresholds', () => {
|
|
657
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
658
|
+
components: [],
|
|
659
|
+
routes: [],
|
|
660
|
+
metrics: {}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const largePayload = 'x'.repeat(2100);
|
|
664
|
+
const routeDependencyIndex = {
|
|
665
|
+
alpha: {
|
|
666
|
+
'deps/shared-large.js': { content: `export const payload = '${largePayload}';` },
|
|
667
|
+
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
668
|
+
},
|
|
669
|
+
beta: {
|
|
670
|
+
'deps/shared-large.js': { content: `export const payload = '${largePayload}';` },
|
|
671
|
+
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
672
|
+
},
|
|
673
|
+
gamma: {
|
|
674
|
+
'deps/shared-small.js': { content: 'export const tiny = 1;' }
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
679
|
+
const sharedSet = generator.computeSharedDependencySet(usageIndex);
|
|
680
|
+
|
|
681
|
+
assert.ok(sharedSet.has('deps/shared-large.js'));
|
|
682
|
+
assert.ok(!sharedSet.has('deps/shared-small.js'));
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('generateVendorSharedDependencyBundleContent emits shared dependency module once', () => {
|
|
686
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
687
|
+
components: [],
|
|
688
|
+
routes: [],
|
|
689
|
+
metrics: {}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const routeDependencyIndex = {
|
|
693
|
+
alpha: {
|
|
694
|
+
'deps/shared.js': { content: `export const payload = '${'y'.repeat(2200)}';` }
|
|
695
|
+
},
|
|
696
|
+
beta: {
|
|
697
|
+
'deps/shared.js': { content: `export const payload = '${'y'.repeat(2200)}';` }
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const usageIndex = generator.indexExternalDependencyUsage(routeDependencyIndex);
|
|
702
|
+
const sharedSet = generator.computeSharedDependencySet(usageIndex);
|
|
703
|
+
generator.vendorShared.dependencyUsage = usageIndex;
|
|
704
|
+
const content = generator.generateVendorSharedDependencyBundleContent(sharedSet);
|
|
705
|
+
const assignmentMatches = content.match(/SLICE_BUNDLE_DEPENDENCIES\["deps\/shared\.js"\]/g) || [];
|
|
706
|
+
|
|
707
|
+
assert.equal(assignmentMatches.length, 1);
|
|
708
|
+
});
|