@esmx/core 3.0.0-rc.116 → 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.
- package/LICENSE +21 -0
- package/README.md +17 -3
- package/README.zh-CN.md +20 -6
- package/dist/app.mjs +4 -2
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.mjs +28 -1
- package/dist/cli/validate.d.ts +15 -0
- package/dist/cli/validate.mjs +139 -0
- package/dist/cli/validate.test.d.ts +1 -0
- package/dist/cli/validate.test.mjs +216 -0
- package/dist/core.d.ts +31 -0
- package/dist/core.mjs +71 -5
- package/dist/declaration/index.d.ts +47 -0
- package/dist/declaration/index.mjs +63 -0
- package/dist/declaration/index.test.d.ts +1 -0
- package/dist/declaration/index.test.mjs +202 -0
- package/dist/declaration/lower.d.ts +11 -0
- package/dist/declaration/lower.mjs +78 -0
- package/dist/declaration/lower.test.d.ts +1 -0
- package/dist/declaration/lower.test.mjs +333 -0
- package/dist/declaration/reader.d.ts +28 -0
- package/dist/declaration/reader.mjs +55 -0
- package/dist/declaration/reinit.e2e.test.d.ts +1 -0
- package/dist/declaration/reinit.e2e.test.mjs +117 -0
- package/dist/declaration/resolver.d.ts +59 -0
- package/dist/declaration/resolver.mjs +430 -0
- package/dist/declaration/resolver.test.d.ts +1 -0
- package/dist/declaration/resolver.test.mjs +1005 -0
- package/dist/declaration/schema.d.ts +89 -0
- package/dist/declaration/schema.mjs +282 -0
- package/dist/declaration/schema.test.d.ts +1 -0
- package/dist/declaration/schema.test.mjs +101 -0
- package/dist/declaration/semver.d.ts +22 -0
- package/dist/declaration/semver.mjs +229 -0
- package/dist/declaration/semver.test.d.ts +1 -0
- package/dist/declaration/semver.test.mjs +87 -0
- package/dist/declaration/test-fixtures.d.ts +18 -0
- package/dist/declaration/test-fixtures.mjs +69 -0
- package/dist/declaration/types.d.ts +58 -0
- package/dist/declaration/types.mjs +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +10 -0
- package/dist/manifest-json.d.ts +61 -0
- package/dist/manifest-json.mjs +68 -5
- package/dist/manifest-json.test.d.ts +1 -0
- package/dist/manifest-json.test.mjs +196 -0
- package/dist/module-config.d.ts +45 -5
- package/dist/module-config.mjs +60 -49
- package/dist/module-config.test.mjs +227 -91
- package/dist/pack-config.d.ts +2 -9
- package/dist/render-context.mjs +22 -5
- package/dist/utils/import-map.d.ts +23 -3
- package/dist/utils/import-map.mjs +38 -1
- package/dist/utils/import-map.test.mjs +228 -0
- package/package.json +9 -6
- package/src/app.ts +5 -2
- package/src/cli/cli.ts +44 -1
- package/src/cli/validate.test.ts +251 -0
- package/src/cli/validate.ts +196 -0
- package/src/core.ts +84 -5
- package/src/declaration/index.test.ts +223 -0
- package/src/declaration/index.ts +135 -0
- package/src/declaration/lower.test.ts +372 -0
- package/src/declaration/lower.ts +135 -0
- package/src/declaration/reader.ts +93 -0
- package/src/declaration/reinit.e2e.test.ts +148 -0
- package/src/declaration/resolver.test.ts +1111 -0
- package/src/declaration/resolver.ts +638 -0
- package/src/declaration/schema.test.ts +118 -0
- package/src/declaration/schema.ts +339 -0
- package/src/declaration/semver.test.ts +101 -0
- package/src/declaration/semver.ts +278 -0
- package/src/declaration/test-fixtures.ts +96 -0
- package/src/declaration/types.ts +71 -0
- package/src/index.ts +28 -1
- package/src/manifest-json.test.ts +236 -0
- package/src/manifest-json.ts +166 -5
- package/src/module-config.test.ts +266 -105
- package/src/module-config.ts +130 -58
- package/src/pack-config.ts +2 -9
- package/src/render-context.ts +34 -6
- package/src/utils/import-map.test.ts +261 -0
- 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
|
+
});
|