@esmx/core 3.0.0-rc.117 → 3.0.0-rc.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +17 -3
  2. package/README.zh-CN.md +20 -6
  3. package/dist/app.mjs +4 -2
  4. package/dist/cli/cli.d.ts +1 -0
  5. package/dist/cli/cli.mjs +28 -1
  6. package/dist/cli/validate.d.ts +15 -0
  7. package/dist/cli/validate.mjs +139 -0
  8. package/dist/cli/validate.test.d.ts +1 -0
  9. package/dist/cli/validate.test.mjs +216 -0
  10. package/dist/core.d.ts +31 -0
  11. package/dist/core.mjs +71 -5
  12. package/dist/declaration/index.d.ts +47 -0
  13. package/dist/declaration/index.mjs +63 -0
  14. package/dist/declaration/index.test.d.ts +1 -0
  15. package/dist/declaration/index.test.mjs +202 -0
  16. package/dist/declaration/lower.d.ts +11 -0
  17. package/dist/declaration/lower.mjs +78 -0
  18. package/dist/declaration/lower.test.d.ts +1 -0
  19. package/dist/declaration/lower.test.mjs +333 -0
  20. package/dist/declaration/reader.d.ts +28 -0
  21. package/dist/declaration/reader.mjs +55 -0
  22. package/dist/declaration/reinit.e2e.test.d.ts +1 -0
  23. package/dist/declaration/reinit.e2e.test.mjs +117 -0
  24. package/dist/declaration/resolver.d.ts +59 -0
  25. package/dist/declaration/resolver.mjs +430 -0
  26. package/dist/declaration/resolver.test.d.ts +1 -0
  27. package/dist/declaration/resolver.test.mjs +1005 -0
  28. package/dist/declaration/schema.d.ts +89 -0
  29. package/dist/declaration/schema.mjs +282 -0
  30. package/dist/declaration/schema.test.d.ts +1 -0
  31. package/dist/declaration/schema.test.mjs +101 -0
  32. package/dist/declaration/semver.d.ts +22 -0
  33. package/dist/declaration/semver.mjs +229 -0
  34. package/dist/declaration/semver.test.d.ts +1 -0
  35. package/dist/declaration/semver.test.mjs +87 -0
  36. package/dist/declaration/test-fixtures.d.ts +18 -0
  37. package/dist/declaration/test-fixtures.mjs +69 -0
  38. package/dist/declaration/types.d.ts +58 -0
  39. package/dist/declaration/types.mjs +21 -0
  40. package/dist/index.d.ts +4 -1
  41. package/dist/index.mjs +10 -0
  42. package/dist/manifest-json.d.ts +61 -0
  43. package/dist/manifest-json.mjs +68 -5
  44. package/dist/manifest-json.test.d.ts +1 -0
  45. package/dist/manifest-json.test.mjs +196 -0
  46. package/dist/module-config.d.ts +45 -5
  47. package/dist/module-config.mjs +60 -49
  48. package/dist/module-config.test.mjs +227 -91
  49. package/dist/pack-config.d.ts +2 -9
  50. package/dist/render-context.mjs +22 -5
  51. package/dist/utils/import-map.d.ts +23 -3
  52. package/dist/utils/import-map.mjs +38 -1
  53. package/dist/utils/import-map.test.mjs +228 -0
  54. package/package.json +9 -6
  55. package/src/app.ts +5 -2
  56. package/src/cli/cli.ts +44 -1
  57. package/src/cli/validate.test.ts +251 -0
  58. package/src/cli/validate.ts +196 -0
  59. package/src/core.ts +84 -5
  60. package/src/declaration/index.test.ts +223 -0
  61. package/src/declaration/index.ts +135 -0
  62. package/src/declaration/lower.test.ts +372 -0
  63. package/src/declaration/lower.ts +135 -0
  64. package/src/declaration/reader.ts +93 -0
  65. package/src/declaration/reinit.e2e.test.ts +148 -0
  66. package/src/declaration/resolver.test.ts +1111 -0
  67. package/src/declaration/resolver.ts +638 -0
  68. package/src/declaration/schema.test.ts +118 -0
  69. package/src/declaration/schema.ts +339 -0
  70. package/src/declaration/semver.test.ts +101 -0
  71. package/src/declaration/semver.ts +278 -0
  72. package/src/declaration/test-fixtures.ts +96 -0
  73. package/src/declaration/types.ts +71 -0
  74. package/src/index.ts +28 -1
  75. package/src/manifest-json.test.ts +236 -0
  76. package/src/manifest-json.ts +166 -5
  77. package/src/module-config.test.ts +266 -105
  78. package/src/module-config.ts +130 -58
  79. package/src/pack-config.ts +2 -9
  80. package/src/render-context.ts +34 -6
  81. package/src/utils/import-map.test.ts +261 -0
  82. package/src/utils/import-map.ts +92 -6
@@ -0,0 +1,1111 @@
1
+ import path from 'node:path';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import type { ReadDeclarationResult } from './reader';
4
+ import { readDeclaration } from './reader';
5
+ import { resolveMounts } from './resolver';
6
+ import {
7
+ createFixtureRoot,
8
+ removeFixtureRoot,
9
+ writeFixturePackage
10
+ } from './test-fixtures';
11
+ import { DiagnosticCode } from './types';
12
+
13
+ const fixtureRoots: string[] = [];
14
+
15
+ async function fixtureRoot(): Promise<string> {
16
+ const root = await createFixtureRoot();
17
+ fixtureRoots.push(root);
18
+ return root;
19
+ }
20
+
21
+ afterEach(async () => {
22
+ await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
23
+ });
24
+
25
+ function readRootDeclaration(appDir: string): ReadDeclarationResult {
26
+ const pkg = readDeclaration(appDir);
27
+ if (!pkg) {
28
+ throw new Error(`fixture app at ${appDir} must declare esmx`);
29
+ }
30
+ return pkg;
31
+ }
32
+
33
+ describe('resolveMounts', () => {
34
+ it('should report E_DUP_PROVIDER when a layer self-provides over a used provider of the same (package, major)', async () => {
35
+ const root = await fixtureRoot();
36
+ writeFixturePackage(root, {
37
+ dir: 'node_modules/base',
38
+ packageJson: {
39
+ name: 'base',
40
+ version: '1.0.0',
41
+ dependencies: { vue: '^3.4.0' },
42
+ esmx: { provides: ['vue'] }
43
+ },
44
+ built: true
45
+ });
46
+ writeFixturePackage(root, {
47
+ dir: 'node_modules/base/node_modules/vue',
48
+ packageJson: { name: 'vue', version: '3.4.21' }
49
+ });
50
+ writeFixturePackage(root, {
51
+ dir: 'node_modules/vue-base',
52
+ packageJson: {
53
+ name: 'vue-base',
54
+ version: '1.0.0',
55
+ dependencies: { vue: '^3.4.0' },
56
+ esmx: { uses: ['base'], provides: ['vue'] }
57
+ },
58
+ built: true
59
+ });
60
+ writeFixturePackage(root, {
61
+ dir: 'node_modules/vue-base/node_modules/vue',
62
+ packageJson: { name: 'vue', version: '3.5.2' }
63
+ });
64
+ const appDir = writeFixturePackage(root, {
65
+ dir: 'app',
66
+ packageJson: {
67
+ name: 'app',
68
+ version: '1.0.0',
69
+ dependencies: { vue: '^3.4.0' },
70
+ esmx: { uses: ['vue-base'] }
71
+ }
72
+ });
73
+
74
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
75
+
76
+ // Two DISTINCT providers of vue@3 (base via uses, vue-base via self):
77
+ // a single-owner conflict, not an election.
78
+ const dupes = result.diagnostics.filter(
79
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
80
+ );
81
+ expect(dupes).toHaveLength(1);
82
+ expect(dupes[0].severity).toBe('error');
83
+ expect(dupes[0].module).toBe('vue-base');
84
+ expect(dupes[0].package).toBe('vue');
85
+ expect(dupes[0].found).toBe('base, vue-base');
86
+ expect(Object.keys(result.mounts).sort()).toEqual(['base', 'vue-base']);
87
+ });
88
+
89
+ it('should report E_DUP_PROVIDER when two distinct providers reach a consumer via separate subtrees', async () => {
90
+ const root = await fixtureRoot();
91
+ for (const [name, provides] of [
92
+ ['x', true],
93
+ ['y', true],
94
+ ['b1', false],
95
+ ['b2', false]
96
+ ] as const) {
97
+ writeFixturePackage(root, {
98
+ dir: `node_modules/${name}`,
99
+ packageJson: {
100
+ name,
101
+ version: '1.0.0',
102
+ esmx: provides
103
+ ? { provides: ['vue'] }
104
+ : { uses: [name === 'b1' ? 'x' : 'y'] }
105
+ },
106
+ built: true
107
+ });
108
+ }
109
+ writeFixturePackage(root, {
110
+ dir: 'node_modules/x/node_modules/vue',
111
+ packageJson: { name: 'vue', version: '3.4.0' }
112
+ });
113
+ writeFixturePackage(root, {
114
+ dir: 'node_modules/y/node_modules/vue',
115
+ packageJson: { name: 'vue', version: '3.5.0' }
116
+ });
117
+ const appDir = writeFixturePackage(root, {
118
+ dir: 'app',
119
+ packageJson: {
120
+ name: 'app',
121
+ version: '1.0.0',
122
+ dependencies: { vue: '^3.4.0' },
123
+ esmx: { uses: ['b1', 'b2'] }
124
+ }
125
+ });
126
+
127
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
128
+
129
+ // x (via b1) and y (via b2) are DISTINCT owners of vue@3 in app's
130
+ // closure — a single-owner conflict surfaces at app.
131
+ const dupes = result.diagnostics.filter(
132
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
133
+ );
134
+ expect(dupes).toHaveLength(1);
135
+ expect(dupes[0].module).toBe('app');
136
+ expect(dupes[0].package).toBe('vue');
137
+ expect(dupes[0].found).toBe('x, y');
138
+ });
139
+
140
+ it('should report E_DUP_PROVIDER when a self-provider collides with a used provider of the same (package, major)', async () => {
141
+ const root = await fixtureRoot();
142
+ writeFixturePackage(root, {
143
+ dir: 'node_modules/base',
144
+ packageJson: {
145
+ name: 'base',
146
+ version: '1.0.0',
147
+ esmx: { provides: ['vue'] }
148
+ },
149
+ built: true
150
+ });
151
+ writeFixturePackage(root, {
152
+ dir: 'node_modules/base/node_modules/vue',
153
+ packageJson: { name: 'vue', version: '3.4.0' }
154
+ });
155
+ writeFixturePackage(root, {
156
+ dir: 'node_modules/vue',
157
+ packageJson: { name: 'vue', version: '3.5.2' }
158
+ });
159
+ const appDir = writeFixturePackage(root, {
160
+ dir: 'app',
161
+ packageJson: {
162
+ name: 'app',
163
+ version: '1.0.0',
164
+ dependencies: { vue: '^3.4.0' },
165
+ esmx: { uses: ['base'], provides: ['vue'] }
166
+ }
167
+ });
168
+
169
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
170
+
171
+ const dupes = result.diagnostics.filter(
172
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
173
+ );
174
+ expect(dupes).toHaveLength(1);
175
+ expect(dupes[0].module).toBe('app');
176
+ expect(dupes[0].package).toBe('vue');
177
+ expect(dupes[0].found).toBe('base, app');
178
+ });
179
+
180
+ it('should report E_TARGET_MISSING when a declared entry/exports target file is absent', async () => {
181
+ const root = await fixtureRoot();
182
+ const appDir = writeFixturePackage(root, {
183
+ dir: 'app',
184
+ packageJson: {
185
+ name: 'app',
186
+ version: '1.0.0',
187
+ esmx: {
188
+ entry: { client: './src/entry.client.ts' },
189
+ exports: { './widget': './src/widget.ts' }
190
+ }
191
+ },
192
+ noSources: true
193
+ });
194
+
195
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
196
+
197
+ const missing = result.diagnostics.filter(
198
+ (d) => d.code === DiagnosticCode.E_TARGET_MISSING
199
+ );
200
+ expect(missing).toHaveLength(2);
201
+ expect(missing.every((d) => d.severity === 'error')).toBe(true);
202
+ expect(missing.map((d) => d.found).sort()).toEqual([
203
+ './src/entry.client.ts',
204
+ './src/widget.ts'
205
+ ]);
206
+ });
207
+
208
+ it('should not report E_TARGET_MISSING when declared targets exist', async () => {
209
+ const root = await fixtureRoot();
210
+ const appDir = writeFixturePackage(root, {
211
+ dir: 'app',
212
+ packageJson: {
213
+ name: 'app',
214
+ version: '1.0.0',
215
+ esmx: { entry: { client: './src/entry.client.ts' } }
216
+ }
217
+ });
218
+
219
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
220
+
221
+ expect(
222
+ result.diagnostics.filter(
223
+ (d) => d.code === DiagnosticCode.E_TARGET_MISSING
224
+ )
225
+ ).toHaveLength(0);
226
+ });
227
+
228
+ it('should report E_CYCLE when the uses chain revisits a module', async () => {
229
+ const root = await fixtureRoot();
230
+ writeFixturePackage(root, {
231
+ dir: 'node_modules/b',
232
+ packageJson: {
233
+ name: 'b',
234
+ version: '1.0.0',
235
+ esmx: { uses: ['a'] }
236
+ },
237
+ built: true
238
+ });
239
+ const appDir = writeFixturePackage(root, {
240
+ dir: 'a',
241
+ packageJson: {
242
+ name: 'a',
243
+ version: '1.0.0',
244
+ esmx: { uses: ['b'] }
245
+ }
246
+ });
247
+
248
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
249
+
250
+ const cycles = result.diagnostics.filter(
251
+ (d) => d.code === DiagnosticCode.E_CYCLE
252
+ );
253
+ expect(cycles).toHaveLength(1);
254
+ expect(cycles[0].severity).toBe('error');
255
+ expect(cycles[0].module).toBe('a');
256
+ });
257
+
258
+ it('should report E_NOT_LINKED for a uses entry with no mount', async () => {
259
+ const root = await fixtureRoot();
260
+ const appDir = writeFixturePackage(root, {
261
+ dir: 'app',
262
+ packageJson: {
263
+ name: 'app',
264
+ version: '1.0.0',
265
+ esmx: { uses: ['ghost'] }
266
+ }
267
+ });
268
+
269
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
270
+
271
+ const notLinked = result.diagnostics.filter(
272
+ (d) => d.code === DiagnosticCode.E_NOT_LINKED
273
+ );
274
+ expect(notLinked).toHaveLength(1);
275
+ expect(notLinked[0].module).toBe('app');
276
+ expect(notLinked[0].package).toBe('ghost');
277
+ expect(result.mounts).toEqual({});
278
+ });
279
+
280
+ it('should report E_NOT_BUILT but keep declaration wiring', async () => {
281
+ const root = await fixtureRoot();
282
+ writeFixturePackage(root, {
283
+ dir: 'node_modules/raw',
284
+ packageJson: {
285
+ name: 'raw',
286
+ version: '1.0.0',
287
+ esmx: { provides: ['vue'] }
288
+ }
289
+ });
290
+ writeFixturePackage(root, {
291
+ dir: 'node_modules/raw/node_modules/vue',
292
+ packageJson: { name: 'vue', version: '3.4.0' }
293
+ });
294
+ const appDir = writeFixturePackage(root, {
295
+ dir: 'app',
296
+ packageJson: {
297
+ name: 'app',
298
+ version: '1.0.0',
299
+ dependencies: { vue: '^3.4.0' },
300
+ esmx: { uses: ['raw'] }
301
+ }
302
+ });
303
+
304
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
305
+
306
+ const notBuilt = result.diagnostics.filter(
307
+ (d) => d.code === DiagnosticCode.E_NOT_BUILT
308
+ );
309
+ expect(notBuilt).toHaveLength(1);
310
+ expect(notBuilt[0].package).toBe('raw');
311
+ expect(result.mounts.raw.built).toBe(false);
312
+ expect(result.supply.vue).toEqual({
313
+ groups: [{ major: 3, provider: 'raw', version: '3.4.0' }]
314
+ });
315
+ });
316
+
317
+ it('should report E_VERSION (intent) when the winner violates a layer range', async () => {
318
+ const root = await fixtureRoot();
319
+ writeFixturePackage(root, {
320
+ dir: 'node_modules/base',
321
+ packageJson: {
322
+ name: 'base',
323
+ version: '1.0.0',
324
+ dependencies: { vue: '^3.0.0' },
325
+ esmx: { provides: ['vue'] }
326
+ },
327
+ built: true
328
+ });
329
+ writeFixturePackage(root, {
330
+ dir: 'node_modules/base/node_modules/vue',
331
+ packageJson: { name: 'vue', version: '3.0.5' }
332
+ });
333
+ const appDir = writeFixturePackage(root, {
334
+ dir: 'app',
335
+ packageJson: {
336
+ name: 'app',
337
+ version: '1.0.0',
338
+ dependencies: { vue: '^3.4.0' },
339
+ esmx: { uses: ['base'] }
340
+ }
341
+ });
342
+
343
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
344
+
345
+ const versionErrors = result.diagnostics.filter(
346
+ (d) => d.code === DiagnosticCode.E_VERSION
347
+ );
348
+ expect(versionErrors).toHaveLength(1);
349
+ expect(versionErrors[0].check).toBe('intent');
350
+ expect(versionErrors[0].module).toBe('app');
351
+ expect(versionErrors[0].package).toBe('vue');
352
+ expect(versionErrors[0].found).toBe('3.0.5');
353
+ expect(versionErrors[0].required).toBe('^3.4.0');
354
+ });
355
+
356
+ it('should report E_VERSION (intent) for used-module version vs range', async () => {
357
+ const root = await fixtureRoot();
358
+ writeFixturePackage(root, {
359
+ dir: 'node_modules/shared',
360
+ packageJson: {
361
+ name: 'shared',
362
+ version: '2.0.0',
363
+ esmx: {}
364
+ },
365
+ built: true
366
+ });
367
+ const appDir = writeFixturePackage(root, {
368
+ dir: 'app',
369
+ packageJson: {
370
+ name: 'app',
371
+ version: '1.0.0',
372
+ dependencies: { shared: '^1.0.0' },
373
+ esmx: { uses: ['shared'] }
374
+ }
375
+ });
376
+
377
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
378
+
379
+ const versionErrors = result.diagnostics.filter(
380
+ (d) => d.code === DiagnosticCode.E_VERSION
381
+ );
382
+ expect(versionErrors).toHaveLength(1);
383
+ expect(versionErrors[0].check).toBe('intent');
384
+ expect(versionErrors[0].package).toBe('shared');
385
+ expect(versionErrors[0].found).toBe('2.0.0');
386
+ });
387
+
388
+ it('should skip the used-module version gate for workspace ranges', async () => {
389
+ const root = await fixtureRoot();
390
+ writeFixturePackage(root, {
391
+ dir: 'node_modules/shared',
392
+ packageJson: {
393
+ name: 'shared',
394
+ version: '2.0.0',
395
+ esmx: {}
396
+ },
397
+ built: true
398
+ });
399
+ const appDir = writeFixturePackage(root, {
400
+ dir: 'app',
401
+ packageJson: {
402
+ name: 'app',
403
+ version: '1.0.0',
404
+ dependencies: { shared: 'workspace:^1.0.0' },
405
+ esmx: { uses: ['shared'] }
406
+ }
407
+ });
408
+
409
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
410
+
411
+ expect(
412
+ result.diagnostics.filter(
413
+ (d) => d.code === DiagnosticCode.E_VERSION
414
+ )
415
+ ).toEqual([]);
416
+ });
417
+
418
+ it('should warn W_NO_RANGE when a layer receives a package without any range', async () => {
419
+ const root = await fixtureRoot();
420
+ writeFixturePackage(root, {
421
+ dir: 'node_modules/base',
422
+ packageJson: {
423
+ name: 'base',
424
+ version: '1.0.0',
425
+ dependencies: { vue: '^3.4.0' },
426
+ esmx: { provides: ['vue'] }
427
+ },
428
+ built: true
429
+ });
430
+ writeFixturePackage(root, {
431
+ dir: 'node_modules/base/node_modules/vue',
432
+ packageJson: { name: 'vue', version: '3.4.21' }
433
+ });
434
+ const appDir = writeFixturePackage(root, {
435
+ dir: 'app',
436
+ packageJson: {
437
+ name: 'app',
438
+ version: '1.0.0',
439
+ esmx: { uses: ['base'] }
440
+ }
441
+ });
442
+
443
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
444
+
445
+ const noRange = result.diagnostics.filter(
446
+ (d) => d.code === DiagnosticCode.W_NO_RANGE
447
+ );
448
+ expect(noRange).toHaveLength(1);
449
+ expect(noRange[0].severity).toBe('warning');
450
+ expect(noRange[0].module).toBe('app');
451
+ expect(noRange[0].package).toBe('vue');
452
+ });
453
+
454
+ it('should warn W_TYPE_DRIFT when the local devDependencies copy diverges from the winner', async () => {
455
+ const root = await fixtureRoot();
456
+ writeFixturePackage(root, {
457
+ dir: 'node_modules/base',
458
+ packageJson: {
459
+ name: 'base',
460
+ version: '1.0.0',
461
+ dependencies: { vue: '^3.4.0' },
462
+ esmx: { provides: ['vue'] }
463
+ },
464
+ built: true
465
+ });
466
+ writeFixturePackage(root, {
467
+ dir: 'node_modules/base/node_modules/vue',
468
+ packageJson: { name: 'vue', version: '3.5.2' }
469
+ });
470
+ const appDir = writeFixturePackage(root, {
471
+ dir: 'app',
472
+ packageJson: {
473
+ name: 'app',
474
+ version: '1.0.0',
475
+ devDependencies: { vue: '^3.4.0' },
476
+ esmx: { uses: ['base'] }
477
+ }
478
+ });
479
+ writeFixturePackage(root, {
480
+ dir: 'app/node_modules/vue',
481
+ packageJson: { name: 'vue', version: '3.4.0' }
482
+ });
483
+
484
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
485
+
486
+ const drift = result.diagnostics.filter(
487
+ (d) => d.code === DiagnosticCode.W_TYPE_DRIFT
488
+ );
489
+ expect(drift).toHaveLength(1);
490
+ expect(drift[0].module).toBe('app');
491
+ expect(drift[0].package).toBe('vue');
492
+ expect(drift[0].found).toBe('3.4.0');
493
+ expect(drift[0].required).toBe('3.5.2');
494
+ });
495
+
496
+ it('should report E_PROTOCOL for a mounted manifest with a higher protocol', async () => {
497
+ const root = await fixtureRoot();
498
+ writeFixturePackage(root, {
499
+ dir: 'node_modules/future',
500
+ packageJson: {
501
+ name: 'future',
502
+ version: '1.0.0',
503
+ esmx: {}
504
+ },
505
+ manifest: { protocol: 3, name: 'future' }
506
+ });
507
+ const appDir = writeFixturePackage(root, {
508
+ dir: 'app',
509
+ packageJson: {
510
+ name: 'app',
511
+ version: '1.0.0',
512
+ esmx: { uses: ['future'] }
513
+ }
514
+ });
515
+
516
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
517
+
518
+ const protocol = result.diagnostics.filter(
519
+ (d) => d.code === DiagnosticCode.E_PROTOCOL
520
+ );
521
+ expect(protocol).toHaveLength(1);
522
+ expect(protocol[0].package).toBe('future');
523
+ expect(protocol[0].found).toBe('3');
524
+ });
525
+
526
+ it('should accept protocol 2 and protocol-less (pre-v2) manifests without E_PROTOCOL', async () => {
527
+ const root = await fixtureRoot();
528
+ writeFixturePackage(root, {
529
+ dir: 'node_modules/modern',
530
+ packageJson: { name: 'modern', version: '1.0.0', esmx: {} },
531
+ manifest: { protocol: 2, name: 'modern' }
532
+ });
533
+ writeFixturePackage(root, {
534
+ dir: 'node_modules/legacy',
535
+ packageJson: { name: 'legacy', version: '1.0.0', esmx: {} },
536
+ manifest: { name: 'legacy' }
537
+ });
538
+ const appDir = writeFixturePackage(root, {
539
+ dir: 'app',
540
+ packageJson: {
541
+ name: 'app',
542
+ version: '1.0.0',
543
+ esmx: { uses: ['modern', 'legacy'] }
544
+ }
545
+ });
546
+
547
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
548
+
549
+ const protocol = result.diagnostics.filter(
550
+ (d) => d.code === DiagnosticCode.E_PROTOCOL
551
+ );
552
+ expect(protocol).toEqual([]);
553
+ });
554
+
555
+ it('should honor envLinks overrides resolved relative to rootDir', async () => {
556
+ const root = await fixtureRoot();
557
+ writeFixturePackage(root, {
558
+ dir: 'shared',
559
+ packageJson: {
560
+ name: 'shared',
561
+ version: '1.0.0',
562
+ esmx: { provides: ['vue'] }
563
+ },
564
+ built: true
565
+ });
566
+ writeFixturePackage(root, {
567
+ dir: 'shared/node_modules/vue',
568
+ packageJson: { name: 'vue', version: '3.4.21' }
569
+ });
570
+ const appDir = writeFixturePackage(root, {
571
+ dir: 'app',
572
+ packageJson: {
573
+ name: 'app',
574
+ version: '1.0.0',
575
+ dependencies: { vue: '^3.4.0' },
576
+ esmx: { uses: ['shared'] }
577
+ }
578
+ });
579
+
580
+ const result = resolveMounts(appDir, readRootDeclaration(appDir), {
581
+ shared: '../shared/dist'
582
+ });
583
+
584
+ expect(result.mounts.shared).toEqual({
585
+ name: 'shared',
586
+ root: path.join(root, 'shared'),
587
+ artifactDir: path.join(root, 'shared/dist'),
588
+ built: true
589
+ });
590
+ expect(result.supply.vue).toEqual({
591
+ groups: [{ major: 3, provider: 'shared', version: '3.4.21' }]
592
+ });
593
+ });
594
+
595
+ it('should keep coexisting majors as isolated groups with W_MULTI_MAJOR and no E_VERSION', async () => {
596
+ const root = await fixtureRoot();
597
+ writeFixturePackage(root, {
598
+ dir: 'node_modules/vue2-app',
599
+ packageJson: {
600
+ name: 'vue2-app',
601
+ version: '1.0.0',
602
+ dependencies: { vue: '2.7.16' },
603
+ esmx: { provides: ['vue'] }
604
+ },
605
+ manifest: { provides: { vue: '2.7.16' } }
606
+ });
607
+ writeFixturePackage(root, {
608
+ dir: 'node_modules/vue2-app/node_modules/vue',
609
+ packageJson: { name: 'vue', version: '2.7.16' }
610
+ });
611
+ writeFixturePackage(root, {
612
+ dir: 'node_modules/vue3-base',
613
+ packageJson: {
614
+ name: 'vue3-base',
615
+ version: '1.0.0',
616
+ dependencies: { vue: '^3.5.0' },
617
+ esmx: { provides: ['vue'] }
618
+ },
619
+ manifest: { provides: { vue: '3.5.13' } }
620
+ });
621
+ writeFixturePackage(root, {
622
+ dir: 'node_modules/vue3-base/node_modules/vue',
623
+ packageJson: { name: 'vue', version: '3.5.13' }
624
+ });
625
+ const appDir = writeFixturePackage(root, {
626
+ dir: 'app',
627
+ packageJson: {
628
+ name: 'app',
629
+ version: '1.0.0',
630
+ peerDependencies: { vue: '^3.5.0' },
631
+ esmx: { uses: ['vue2-app', 'vue3-base'] }
632
+ }
633
+ });
634
+
635
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
636
+
637
+ expect(result.supply.vue).toEqual({
638
+ groups: [
639
+ { major: 3, provider: 'vue3-base', version: '3.5.13' },
640
+ { major: 2, provider: 'vue2-app', version: '2.7.16' }
641
+ ]
642
+ });
643
+ const errors = result.diagnostics.filter((d) => d.severity === 'error');
644
+ expect(errors).toEqual([]);
645
+ const multiMajor = result.diagnostics.filter(
646
+ (d) => d.code === DiagnosticCode.W_MULTI_MAJOR
647
+ );
648
+ expect(multiMajor).toHaveLength(1);
649
+ expect(multiMajor[0].package).toBe('vue');
650
+ expect(multiMajor[0].found).toContain('vue3-base');
651
+ expect(multiMajor[0].found).toContain('vue2-app');
652
+ });
653
+
654
+ it('should let different majors coexist across a uses chain with no E_DUP_PROVIDER', async () => {
655
+ const root = await fixtureRoot();
656
+ writeFixturePackage(root, {
657
+ dir: 'node_modules/base',
658
+ packageJson: {
659
+ name: 'base',
660
+ version: '1.0.0',
661
+ dependencies: { vue: '^2.6.0' },
662
+ esmx: { provides: ['vue'] }
663
+ },
664
+ manifest: { provides: { vue: '2.6.14' } }
665
+ });
666
+ writeFixturePackage(root, {
667
+ dir: 'node_modules/base/node_modules/vue',
668
+ packageJson: { name: 'vue', version: '2.6.14' }
669
+ });
670
+ writeFixturePackage(root, {
671
+ dir: 'node_modules/vue-base',
672
+ packageJson: {
673
+ name: 'vue-base',
674
+ version: '1.0.0',
675
+ dependencies: { vue: '^3.4.0' },
676
+ esmx: { uses: ['base'], provides: ['vue'] }
677
+ },
678
+ built: true
679
+ });
680
+ writeFixturePackage(root, {
681
+ dir: 'node_modules/vue-base/node_modules/vue',
682
+ packageJson: { name: 'vue', version: '3.5.2' }
683
+ });
684
+ const appDir = writeFixturePackage(root, {
685
+ dir: 'app',
686
+ packageJson: {
687
+ name: 'app',
688
+ version: '1.0.0',
689
+ dependencies: { vue: '^3.4.0' },
690
+ esmx: { uses: ['vue-base'] }
691
+ }
692
+ });
693
+
694
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
695
+
696
+ // base owns vue@2, vue-base owns vue@3 — distinct (package, major)
697
+ // groups, so single-owner is satisfied per major: no conflict.
698
+ expect(
699
+ result.diagnostics.filter(
700
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
701
+ )
702
+ ).toEqual([]);
703
+ expect(
704
+ result.diagnostics.filter((d) => d.severity === 'error')
705
+ ).toEqual([]);
706
+ expect(result.supply.vue.groups).toHaveLength(2);
707
+ expect(
708
+ result.diagnostics.filter(
709
+ (d) => d.code === DiagnosticCode.W_MULTI_MAJOR
710
+ )
711
+ ).toHaveLength(1);
712
+ });
713
+
714
+ it('should validate intent against the group a layer wires to, not an unrelated major', async () => {
715
+ const root = await fixtureRoot();
716
+ writeFixturePackage(root, {
717
+ dir: 'node_modules/vue2-app',
718
+ packageJson: {
719
+ name: 'vue2-app',
720
+ version: '1.0.0',
721
+ dependencies: { vue: '2.7.16' },
722
+ esmx: { provides: ['vue'] }
723
+ },
724
+ built: true
725
+ });
726
+ writeFixturePackage(root, {
727
+ dir: 'node_modules/vue2-app/node_modules/vue',
728
+ packageJson: { name: 'vue', version: '2.7.16' }
729
+ });
730
+ writeFixturePackage(root, {
731
+ dir: 'node_modules/vue3-base',
732
+ packageJson: {
733
+ name: 'vue3-base',
734
+ version: '1.0.0',
735
+ dependencies: { vue: '^3.5.0' },
736
+ esmx: { provides: ['vue'] }
737
+ },
738
+ built: true
739
+ });
740
+ writeFixturePackage(root, {
741
+ dir: 'node_modules/vue3-base/node_modules/vue',
742
+ packageJson: { name: 'vue', version: '3.5.13' }
743
+ });
744
+ const appDir = writeFixturePackage(root, {
745
+ dir: 'app',
746
+ packageJson: {
747
+ name: 'app',
748
+ version: '1.0.0',
749
+ dependencies: { vue: '^2.7.0' },
750
+ esmx: { uses: ['vue3-base', 'vue2-app'] }
751
+ }
752
+ });
753
+
754
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
755
+
756
+ // app's ^2.7.0 range wires it to the major-2 group; the 3.x group
757
+ // existing must not produce an intent violation.
758
+ expect(
759
+ result.diagnostics.filter(
760
+ (d) => d.code === DiagnosticCode.E_VERSION
761
+ )
762
+ ).toEqual([]);
763
+ });
764
+
765
+ it('should report E_VERSION (intent) when no major group satisfies the range', async () => {
766
+ const root = await fixtureRoot();
767
+ writeFixturePackage(root, {
768
+ dir: 'node_modules/vue2-app',
769
+ packageJson: {
770
+ name: 'vue2-app',
771
+ version: '1.0.0',
772
+ dependencies: { vue: '2.7.16' },
773
+ esmx: { provides: ['vue'] }
774
+ },
775
+ built: true
776
+ });
777
+ writeFixturePackage(root, {
778
+ dir: 'node_modules/vue2-app/node_modules/vue',
779
+ packageJson: { name: 'vue', version: '2.7.16' }
780
+ });
781
+ writeFixturePackage(root, {
782
+ dir: 'node_modules/vue3-base',
783
+ packageJson: {
784
+ name: 'vue3-base',
785
+ version: '1.0.0',
786
+ dependencies: { vue: '^3.5.0' },
787
+ esmx: { provides: ['vue'] }
788
+ },
789
+ built: true
790
+ });
791
+ writeFixturePackage(root, {
792
+ dir: 'node_modules/vue3-base/node_modules/vue',
793
+ packageJson: { name: 'vue', version: '3.5.13' }
794
+ });
795
+ const appDir = writeFixturePackage(root, {
796
+ dir: 'app',
797
+ packageJson: {
798
+ name: 'app',
799
+ version: '1.0.0',
800
+ dependencies: { vue: '^4.0.0' },
801
+ esmx: { uses: ['vue2-app', 'vue3-base'] }
802
+ }
803
+ });
804
+
805
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
806
+
807
+ const versionErrors = result.diagnostics.filter(
808
+ (d) => d.code === DiagnosticCode.E_VERSION
809
+ );
810
+ expect(versionErrors).toHaveLength(1);
811
+ expect(versionErrors[0].check).toBe('intent');
812
+ expect(versionErrors[0].module).toBe('app');
813
+ expect(versionErrors[0].required).toBe('^4.0.0');
814
+ });
815
+
816
+ it('should resolve the hub-shaped topology (vue2 + multiple vue3 providers) with warnings only', async () => {
817
+ const root = await fixtureRoot();
818
+ writeFixturePackage(root, {
819
+ dir: 'node_modules/shared',
820
+ packageJson: {
821
+ name: 'shared',
822
+ version: '1.0.0',
823
+ dependencies: { unhead: '^3.1.3' },
824
+ esmx: { provides: ['unhead'] }
825
+ },
826
+ manifest: { provides: { unhead: '3.1.3' } }
827
+ });
828
+ writeFixturePackage(root, {
829
+ dir: 'node_modules/shared/node_modules/unhead',
830
+ packageJson: { name: 'unhead', version: '3.1.3' }
831
+ });
832
+ writeFixturePackage(root, {
833
+ dir: 'node_modules/vue2-app',
834
+ packageJson: {
835
+ name: 'vue2-app',
836
+ version: '1.0.0',
837
+ dependencies: { vue: '2.7.16' },
838
+ esmx: { uses: ['shared'], provides: ['vue'] }
839
+ },
840
+ manifest: { provides: { vue: '2.7.16' } }
841
+ });
842
+ writeFixturePackage(root, {
843
+ dir: 'node_modules/vue2-app/node_modules/vue',
844
+ packageJson: { name: 'vue', version: '2.7.16' }
845
+ });
846
+ const vue3Providers = ['vue3-app', 'vite-vue-app', 'rsbuild-vue-app'];
847
+ for (const name of vue3Providers) {
848
+ writeFixturePackage(root, {
849
+ dir: `node_modules/${name}`,
850
+ packageJson: {
851
+ name,
852
+ version: '1.0.0',
853
+ dependencies: { vue: '^3.5.0' },
854
+ esmx: { uses: ['shared'], provides: ['vue'] }
855
+ },
856
+ manifest: { provides: { vue: '3.5.40' } }
857
+ });
858
+ writeFixturePackage(root, {
859
+ dir: `node_modules/${name}/node_modules/vue`,
860
+ packageJson: { name: 'vue', version: '3.5.40' }
861
+ });
862
+ }
863
+ const passiveApps: string[] = [];
864
+ for (let i = 0; i < 11; i++) {
865
+ const name = `app-${i}`;
866
+ passiveApps.push(name);
867
+ writeFixturePackage(root, {
868
+ dir: `node_modules/${name}`,
869
+ packageJson: {
870
+ name,
871
+ version: '1.0.0',
872
+ peerDependencies: { unhead: '^3.1.3' },
873
+ esmx: { uses: ['shared'] }
874
+ },
875
+ built: true
876
+ });
877
+ }
878
+ const appDir = writeFixturePackage(root, {
879
+ dir: 'hub',
880
+ packageJson: {
881
+ name: 'hub',
882
+ version: '1.0.0',
883
+ peerDependencies: { unhead: '^3.1.3', vue: '^3.5.34' },
884
+ esmx: {
885
+ uses: [
886
+ 'shared',
887
+ ...passiveApps,
888
+ 'vue2-app',
889
+ ...vue3Providers
890
+ ]
891
+ }
892
+ }
893
+ });
894
+
895
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
896
+
897
+ // Three distinct providers of vue@3 (vue3-app, vite-vue-app,
898
+ // rsbuild-vue-app) violate single-owner: E_DUP_PROVIDER fires once
899
+ // (de-duped per (package, major)). vue2-app alone owns vue@2 cleanly.
900
+ const dupes = result.diagnostics.filter(
901
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
902
+ );
903
+ expect(dupes).toHaveLength(1);
904
+ expect(dupes[0].module).toBe('hub');
905
+ expect(dupes[0].package).toBe('vue');
906
+ expect(dupes[0].found).toBe('vue3-app, vite-vue-app');
907
+ const codes = result.diagnostics.map((d) => d.code);
908
+ expect(codes).toContain(DiagnosticCode.W_MULTI_MAJOR);
909
+ });
910
+
911
+ it('should report a single de-duped E_DUP_PROVIDER for a 3-provider same-major collision', async () => {
912
+ const root = await fixtureRoot();
913
+ for (const name of ['pa', 'pb', 'pc'] as const) {
914
+ writeFixturePackage(root, {
915
+ dir: `node_modules/${name}`,
916
+ packageJson: {
917
+ name,
918
+ version: '1.0.0',
919
+ esmx: { provides: ['vue'] }
920
+ },
921
+ built: true
922
+ });
923
+ writeFixturePackage(root, {
924
+ dir: `node_modules/${name}/node_modules/vue`,
925
+ packageJson: { name: 'vue', version: '3.5.0' }
926
+ });
927
+ }
928
+ const appDir = writeFixturePackage(root, {
929
+ dir: 'app',
930
+ packageJson: {
931
+ name: 'app',
932
+ version: '1.0.0',
933
+ dependencies: { vue: '^3.5.0' },
934
+ esmx: { uses: ['pa', 'pb', 'pc'] }
935
+ }
936
+ });
937
+
938
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
939
+
940
+ // pa, pb, pc all own vue@3 — a single (package, major) conflict,
941
+ // reported once at the consumer naming the first two colliders.
942
+ const dupes = result.diagnostics.filter(
943
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
944
+ );
945
+ expect(dupes).toHaveLength(1);
946
+ expect(dupes[0].module).toBe('app');
947
+ expect(dupes[0].package).toBe('vue');
948
+ expect(dupes[0].found).toBe('pa, pb');
949
+ });
950
+
951
+ it('should withhold supply when a uses cycle also provides', async () => {
952
+ const root = await fixtureRoot();
953
+ // a ↔ b cycle, both provide vue. The merge result during a cyclic walk
954
+ // depends on traversal-entry order, so RFC P3 hard-stops: E_CYCLE fires
955
+ // and supply is withheld rather than wired on a coin-flip.
956
+ writeFixturePackage(root, {
957
+ dir: 'node_modules/a',
958
+ packageJson: {
959
+ name: 'a',
960
+ version: '1.0.0',
961
+ esmx: { uses: ['b'], provides: ['vue'] }
962
+ },
963
+ built: true
964
+ });
965
+ writeFixturePackage(root, {
966
+ dir: 'node_modules/a/node_modules/vue',
967
+ packageJson: { name: 'vue', version: '3.4.0' }
968
+ });
969
+ writeFixturePackage(root, {
970
+ dir: 'node_modules/b',
971
+ packageJson: {
972
+ name: 'b',
973
+ version: '1.0.0',
974
+ esmx: { uses: ['a'], provides: ['vue'] }
975
+ },
976
+ built: true
977
+ });
978
+ writeFixturePackage(root, {
979
+ dir: 'node_modules/b/node_modules/vue',
980
+ packageJson: { name: 'vue', version: '3.4.0' }
981
+ });
982
+ const appDir = writeFixturePackage(root, {
983
+ dir: 'app',
984
+ packageJson: {
985
+ name: 'app',
986
+ version: '1.0.0',
987
+ dependencies: { vue: '^3.4.0' },
988
+ esmx: { uses: ['a'] }
989
+ }
990
+ });
991
+
992
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
993
+
994
+ expect(
995
+ result.diagnostics.some((d) => d.code === DiagnosticCode.E_CYCLE)
996
+ ).toBe(true);
997
+ // Supply is withheld entirely (not an arbitrary order-dependent winner):
998
+ // the E_CYCLE error fails the build instead of emitting coin-flip wiring.
999
+ expect(result.supply).toEqual({});
1000
+ });
1001
+
1002
+ it('should report E_DUP_PROVIDER when base and a sibling both provide vue@3 in the same closure', async () => {
1003
+ const root = await fixtureRoot();
1004
+ // base provides vue@3; sibling (used after base) also provides vue@3.
1005
+ // Both reach the app's closure — single-owner per (package, major) is
1006
+ // violated, so it is a hard error, not an election.
1007
+ writeFixturePackage(root, {
1008
+ dir: 'node_modules/base',
1009
+ packageJson: {
1010
+ name: 'base',
1011
+ version: '1.0.0',
1012
+ dependencies: { vue: '^3.4.0' },
1013
+ esmx: { provides: ['vue'] }
1014
+ },
1015
+ built: true
1016
+ });
1017
+ writeFixturePackage(root, {
1018
+ dir: 'node_modules/base/node_modules/vue',
1019
+ packageJson: { name: 'vue', version: '3.4.21' }
1020
+ });
1021
+ writeFixturePackage(root, {
1022
+ dir: 'node_modules/sibling',
1023
+ packageJson: {
1024
+ name: 'sibling',
1025
+ version: '1.0.0',
1026
+ dependencies: { vue: '^3.4.0' },
1027
+ esmx: { provides: ['vue'] }
1028
+ },
1029
+ built: true
1030
+ });
1031
+ writeFixturePackage(root, {
1032
+ dir: 'node_modules/sibling/node_modules/vue',
1033
+ packageJson: { name: 'vue', version: '3.5.2' }
1034
+ });
1035
+ const appDir = writeFixturePackage(root, {
1036
+ dir: 'app',
1037
+ packageJson: {
1038
+ name: 'app',
1039
+ version: '1.0.0',
1040
+ dependencies: { vue: '^3.4.0' },
1041
+ esmx: { uses: ['base', 'sibling'] }
1042
+ }
1043
+ });
1044
+
1045
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
1046
+
1047
+ const dupes = result.diagnostics.filter(
1048
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
1049
+ );
1050
+ expect(dupes).toHaveLength(1);
1051
+ expect(dupes[0].severity).toBe('error');
1052
+ expect(dupes[0].module).toBe('app');
1053
+ expect(dupes[0].package).toBe('vue');
1054
+ expect(dupes[0].found).toBe('base, sibling');
1055
+ });
1056
+
1057
+ it('should not report E_DUP_PROVIDER when the SAME provider reaches via two diamond paths', async () => {
1058
+ const root = await fixtureRoot();
1059
+ // base owns vue@3. Two intermediaries (left, right) both use base, and
1060
+ // the app uses both — base reaches via two routes but is a single
1061
+ // owner, so the diamond resolves cleanly with no conflict.
1062
+ writeFixturePackage(root, {
1063
+ dir: 'node_modules/base',
1064
+ packageJson: {
1065
+ name: 'base',
1066
+ version: '1.0.0',
1067
+ dependencies: { vue: '^3.4.0' },
1068
+ esmx: { provides: ['vue'] }
1069
+ },
1070
+ built: true
1071
+ });
1072
+ writeFixturePackage(root, {
1073
+ dir: 'node_modules/base/node_modules/vue',
1074
+ packageJson: { name: 'vue', version: '3.5.2' }
1075
+ });
1076
+ for (const name of ['left', 'right'] as const) {
1077
+ writeFixturePackage(root, {
1078
+ dir: `node_modules/${name}`,
1079
+ packageJson: {
1080
+ name,
1081
+ version: '1.0.0',
1082
+ esmx: { uses: ['base'] }
1083
+ },
1084
+ built: true
1085
+ });
1086
+ }
1087
+ const appDir = writeFixturePackage(root, {
1088
+ dir: 'app',
1089
+ packageJson: {
1090
+ name: 'app',
1091
+ version: '1.0.0',
1092
+ dependencies: { vue: '^3.4.0' },
1093
+ esmx: { uses: ['left', 'right'] }
1094
+ }
1095
+ });
1096
+
1097
+ const result = resolveMounts(appDir, readRootDeclaration(appDir));
1098
+
1099
+ expect(
1100
+ result.diagnostics.filter(
1101
+ (d) => d.code === DiagnosticCode.E_DUP_PROVIDER
1102
+ )
1103
+ ).toEqual([]);
1104
+ expect(
1105
+ result.diagnostics.filter((d) => d.severity === 'error')
1106
+ ).toEqual([]);
1107
+ expect(result.supply.vue).toEqual({
1108
+ groups: [{ major: 3, provider: 'base', version: '3.5.2' }]
1109
+ });
1110
+ });
1111
+ });