@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.
- 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,333 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { parseModuleConfig } from "../module-config.mjs";
|
|
4
|
+
import { lowerDeclaration } from "./lower.mjs";
|
|
5
|
+
import { readDeclaration } from "./reader.mjs";
|
|
6
|
+
import { resolveMounts } from "./resolver.mjs";
|
|
7
|
+
import {
|
|
8
|
+
createFixtureRoot,
|
|
9
|
+
removeFixtureRoot,
|
|
10
|
+
writeFixturePackage
|
|
11
|
+
} from "./test-fixtures.mjs";
|
|
12
|
+
const fixtureRoots = [];
|
|
13
|
+
async function fixtureRoot() {
|
|
14
|
+
const root = await createFixtureRoot();
|
|
15
|
+
fixtureRoots.push(root);
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
|
|
20
|
+
});
|
|
21
|
+
function readRootDeclaration(appDir) {
|
|
22
|
+
const pkg = readDeclaration(appDir);
|
|
23
|
+
if (!pkg) {
|
|
24
|
+
throw new Error(`fixture app at ${appDir} must declare esmx`);
|
|
25
|
+
}
|
|
26
|
+
return pkg;
|
|
27
|
+
}
|
|
28
|
+
describe("lowerDeclaration", () => {
|
|
29
|
+
it("should lower the ssr-micro-react-equivalent declaration to parity with the legacy config", async () => {
|
|
30
|
+
const root = await fixtureRoot();
|
|
31
|
+
writeFixturePackage(root, {
|
|
32
|
+
dir: "shared",
|
|
33
|
+
packageJson: {
|
|
34
|
+
name: "ssr-micro-shared",
|
|
35
|
+
version: "1.0.0",
|
|
36
|
+
esmx: { provides: ["@esmx/router"] }
|
|
37
|
+
},
|
|
38
|
+
built: true
|
|
39
|
+
});
|
|
40
|
+
writeFixturePackage(root, {
|
|
41
|
+
dir: "shared/node_modules/@esmx/router",
|
|
42
|
+
packageJson: { name: "@esmx/router", version: "1.2.3" }
|
|
43
|
+
});
|
|
44
|
+
const appDir = writeFixturePackage(root, {
|
|
45
|
+
dir: "app",
|
|
46
|
+
packageJson: {
|
|
47
|
+
name: "ssr-micro-react",
|
|
48
|
+
version: "1.0.0",
|
|
49
|
+
devDependencies: {
|
|
50
|
+
"@esmx/router": "*",
|
|
51
|
+
react: "*",
|
|
52
|
+
"react-dom": "*"
|
|
53
|
+
},
|
|
54
|
+
esmx: {
|
|
55
|
+
entry: {
|
|
56
|
+
client: "./src/entry.client.ts",
|
|
57
|
+
server: "./src/entry.server.ts"
|
|
58
|
+
},
|
|
59
|
+
exports: { "./src/routes": "./src/routes.ts" },
|
|
60
|
+
provides: ["react", "react-dom"],
|
|
61
|
+
uses: ["ssr-micro-shared"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const pkg = readRootDeclaration(appDir);
|
|
66
|
+
const resolution = resolveMounts(appDir, pkg, {
|
|
67
|
+
"ssr-micro-shared": "../shared/dist"
|
|
68
|
+
});
|
|
69
|
+
const legacy = {
|
|
70
|
+
links: {
|
|
71
|
+
"ssr-micro-shared": path.join(root, "shared/dist")
|
|
72
|
+
},
|
|
73
|
+
imports: {
|
|
74
|
+
"@esmx/router": "ssr-micro-shared/@esmx/router"
|
|
75
|
+
},
|
|
76
|
+
exports: ["pkg:react", "pkg:react-dom", "root:src/routes.ts"]
|
|
77
|
+
};
|
|
78
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
79
|
+
expect(lowered.imports).toEqual({
|
|
80
|
+
"@esmx/router": "ssr-micro-shared/@esmx/router"
|
|
81
|
+
});
|
|
82
|
+
expect(parseModuleConfig("ssr-micro-react", appDir, lowered)).toEqual(
|
|
83
|
+
parseModuleConfig("ssr-micro-react", appDir, legacy)
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
it("should set lib:true when no entry is declared", async () => {
|
|
87
|
+
const root = await fixtureRoot();
|
|
88
|
+
const appDir = writeFixturePackage(root, {
|
|
89
|
+
dir: "lib-module",
|
|
90
|
+
packageJson: {
|
|
91
|
+
name: "lib-module",
|
|
92
|
+
version: "1.0.0",
|
|
93
|
+
esmx: { exports: { "./widget": "./src/widget.ts" } }
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const pkg = readRootDeclaration(appDir);
|
|
97
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
98
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
99
|
+
expect(lowered.lib).toBe(true);
|
|
100
|
+
expect(lowered.exports).toEqual([{ widget: "root:src/widget.ts" }]);
|
|
101
|
+
expect(lowered.links).toBeUndefined();
|
|
102
|
+
expect(lowered.imports).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
it("should lower custom entries through config.entry with path-derived names", async () => {
|
|
105
|
+
const root = await fixtureRoot();
|
|
106
|
+
const appDir = writeFixturePackage(root, {
|
|
107
|
+
dir: "custom-entry",
|
|
108
|
+
packageJson: {
|
|
109
|
+
name: "custom-entry",
|
|
110
|
+
version: "1.0.0",
|
|
111
|
+
esmx: {
|
|
112
|
+
entry: {
|
|
113
|
+
client: "./src/main.client.ts",
|
|
114
|
+
server: "./src/entry.server.ts"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const pkg = readRootDeclaration(appDir);
|
|
120
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
121
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
122
|
+
expect(lowered.lib).toBeUndefined();
|
|
123
|
+
expect(lowered.entry).toEqual({ client: "./src/main.client.ts" });
|
|
124
|
+
expect(lowered.exports).toBeUndefined();
|
|
125
|
+
const parsed = parseModuleConfig("custom-entry", appDir, lowered);
|
|
126
|
+
expect(parsed.entry).toEqual({
|
|
127
|
+
client: { name: "src/main.client", file: "./src/main.client.ts" },
|
|
128
|
+
server: { name: "src/entry.server", file: "./src/entry.server" }
|
|
129
|
+
});
|
|
130
|
+
expect(parsed.environments.client.exports["src/main.client"]).toEqual({
|
|
131
|
+
name: "src/main.client",
|
|
132
|
+
file: "./src/main.client.ts",
|
|
133
|
+
pkg: false
|
|
134
|
+
});
|
|
135
|
+
expect(parsed.environments.server.exports["src/entry.server"]).toEqual({
|
|
136
|
+
name: "src/entry.server",
|
|
137
|
+
file: "./src/entry.server",
|
|
138
|
+
pkg: false
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
it("should disable an undeclared entry side", async () => {
|
|
142
|
+
const root = await fixtureRoot();
|
|
143
|
+
const appDir = writeFixturePackage(root, {
|
|
144
|
+
dir: "client-only",
|
|
145
|
+
packageJson: {
|
|
146
|
+
name: "client-only",
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
esmx: { entry: { client: "./src/entry.client.ts" } }
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const pkg = readRootDeclaration(appDir);
|
|
152
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
153
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
154
|
+
expect(lowered.entry).toEqual({ server: false });
|
|
155
|
+
const parsed = parseModuleConfig("client-only", appDir, lowered);
|
|
156
|
+
expect(parsed.entry.server).toBeNull();
|
|
157
|
+
expect(
|
|
158
|
+
parsed.environments.server.exports["src/entry.server"]
|
|
159
|
+
).toBeUndefined();
|
|
160
|
+
expect(
|
|
161
|
+
parsed.environments.client.exports["src/entry.client"].file
|
|
162
|
+
).toBe("./src/entry.client");
|
|
163
|
+
});
|
|
164
|
+
it("should preserve env forks including false in exports", async () => {
|
|
165
|
+
const root = await fixtureRoot();
|
|
166
|
+
const appDir = writeFixturePackage(root, {
|
|
167
|
+
dir: "forked",
|
|
168
|
+
packageJson: {
|
|
169
|
+
name: "forked",
|
|
170
|
+
version: "1.0.0",
|
|
171
|
+
esmx: {
|
|
172
|
+
exports: {
|
|
173
|
+
"./store": {
|
|
174
|
+
client: "./src/store.client.ts",
|
|
175
|
+
server: false
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const pkg = readRootDeclaration(appDir);
|
|
182
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
183
|
+
const lowered = lowerDeclaration(pkg, resolution);
|
|
184
|
+
expect(lowered.exports).toEqual([
|
|
185
|
+
{
|
|
186
|
+
store: {
|
|
187
|
+
client: "root:src/store.client.ts",
|
|
188
|
+
server: false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
]);
|
|
192
|
+
const parsed = parseModuleConfig("forked", appDir, lowered);
|
|
193
|
+
expect(parsed.environments.client.exports.store.file).toBe(
|
|
194
|
+
"./src/store.client.ts"
|
|
195
|
+
);
|
|
196
|
+
expect(parsed.environments.server.exports.store.file).toBe("");
|
|
197
|
+
});
|
|
198
|
+
it("should wire a single-owner supply group into config.imports", async () => {
|
|
199
|
+
const root = await fixtureRoot();
|
|
200
|
+
writeFixturePackage(root, {
|
|
201
|
+
dir: "node_modules/shared",
|
|
202
|
+
packageJson: {
|
|
203
|
+
name: "shared",
|
|
204
|
+
version: "1.0.0",
|
|
205
|
+
dependencies: { vue: "^3.4.0" },
|
|
206
|
+
esmx: { provides: ["vue"] }
|
|
207
|
+
},
|
|
208
|
+
built: true
|
|
209
|
+
});
|
|
210
|
+
writeFixturePackage(root, {
|
|
211
|
+
dir: "node_modules/shared/node_modules/vue",
|
|
212
|
+
packageJson: { name: "vue", version: "3.5.2" }
|
|
213
|
+
});
|
|
214
|
+
const appDir = writeFixturePackage(root, {
|
|
215
|
+
dir: "app",
|
|
216
|
+
packageJson: {
|
|
217
|
+
name: "app",
|
|
218
|
+
version: "1.0.0",
|
|
219
|
+
dependencies: { vue: "^3.4.0" },
|
|
220
|
+
esmx: { uses: ["shared"] }
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
const pkg = readRootDeclaration(appDir);
|
|
224
|
+
const lowered = lowerDeclaration(pkg, resolveMounts(appDir, pkg));
|
|
225
|
+
expect(lowered.imports).toEqual({ vue: "shared/vue" });
|
|
226
|
+
});
|
|
227
|
+
describe("per-major group wiring", () => {
|
|
228
|
+
async function multiMajorFixture() {
|
|
229
|
+
const root = await fixtureRoot();
|
|
230
|
+
writeFixturePackage(root, {
|
|
231
|
+
dir: "node_modules/vue2-app",
|
|
232
|
+
packageJson: {
|
|
233
|
+
name: "vue2-app",
|
|
234
|
+
version: "1.0.0",
|
|
235
|
+
dependencies: { vue: "2.7.16" },
|
|
236
|
+
esmx: { provides: ["vue"] }
|
|
237
|
+
},
|
|
238
|
+
built: true
|
|
239
|
+
});
|
|
240
|
+
writeFixturePackage(root, {
|
|
241
|
+
dir: "node_modules/vue2-app/node_modules/vue",
|
|
242
|
+
packageJson: { name: "vue", version: "2.7.16" }
|
|
243
|
+
});
|
|
244
|
+
writeFixturePackage(root, {
|
|
245
|
+
dir: "node_modules/vue3-base",
|
|
246
|
+
packageJson: {
|
|
247
|
+
name: "vue3-base",
|
|
248
|
+
version: "1.0.0",
|
|
249
|
+
dependencies: { vue: "^3.5.0" },
|
|
250
|
+
esmx: { provides: ["vue"] }
|
|
251
|
+
},
|
|
252
|
+
built: true
|
|
253
|
+
});
|
|
254
|
+
writeFixturePackage(root, {
|
|
255
|
+
dir: "node_modules/vue3-base/node_modules/vue",
|
|
256
|
+
packageJson: { name: "vue", version: "3.5.13" }
|
|
257
|
+
});
|
|
258
|
+
return root;
|
|
259
|
+
}
|
|
260
|
+
function lowerApp(appDir) {
|
|
261
|
+
const pkg = readRootDeclaration(appDir);
|
|
262
|
+
const resolution = resolveMounts(appDir, pkg);
|
|
263
|
+
return { resolution, lowered: lowerDeclaration(pkg, resolution) };
|
|
264
|
+
}
|
|
265
|
+
it("should wire a ^3 consumer to the vue3 group winner", async () => {
|
|
266
|
+
const root = await multiMajorFixture();
|
|
267
|
+
const appDir = writeFixturePackage(root, {
|
|
268
|
+
dir: "app",
|
|
269
|
+
packageJson: {
|
|
270
|
+
name: "app",
|
|
271
|
+
version: "1.0.0",
|
|
272
|
+
peerDependencies: { vue: "^3.5.0" },
|
|
273
|
+
esmx: { uses: ["vue2-app", "vue3-base"] }
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
const { lowered } = lowerApp(appDir);
|
|
277
|
+
expect(lowered.imports?.vue).toBe("vue3-base/vue");
|
|
278
|
+
});
|
|
279
|
+
it("should wire a ^2 consumer to the vue2 group winner", async () => {
|
|
280
|
+
const root = await multiMajorFixture();
|
|
281
|
+
const appDir = writeFixturePackage(root, {
|
|
282
|
+
dir: "app",
|
|
283
|
+
packageJson: {
|
|
284
|
+
name: "app",
|
|
285
|
+
version: "1.0.0",
|
|
286
|
+
dependencies: { vue: "^2.7.0" },
|
|
287
|
+
esmx: { uses: ["vue2-app", "vue3-base"] }
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
const { lowered } = lowerApp(appDir);
|
|
291
|
+
expect(lowered.imports?.vue).toBe("vue2-app/vue");
|
|
292
|
+
});
|
|
293
|
+
it("should wire a range-less consumer to the highest major with W_NO_RANGE", async () => {
|
|
294
|
+
const root = await multiMajorFixture();
|
|
295
|
+
const appDir = writeFixturePackage(root, {
|
|
296
|
+
dir: "app",
|
|
297
|
+
packageJson: {
|
|
298
|
+
name: "app",
|
|
299
|
+
version: "1.0.0",
|
|
300
|
+
esmx: { uses: ["vue2-app", "vue3-base"] }
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
const { resolution, lowered } = lowerApp(appDir);
|
|
304
|
+
expect(lowered.imports?.vue).toBe("vue3-base/vue");
|
|
305
|
+
expect(
|
|
306
|
+
resolution.diagnostics.some(
|
|
307
|
+
(d) => d.code === "W_NO_RANGE" && d.package === "vue"
|
|
308
|
+
)
|
|
309
|
+
).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
it("should let a 2.x provider wire to its own copy despite a 3.x group existing", async () => {
|
|
312
|
+
const root = await multiMajorFixture();
|
|
313
|
+
const appDir = writeFixturePackage(root, {
|
|
314
|
+
dir: "app",
|
|
315
|
+
packageJson: {
|
|
316
|
+
name: "app",
|
|
317
|
+
version: "1.0.0",
|
|
318
|
+
dependencies: { vue: "2.7.16" },
|
|
319
|
+
esmx: { uses: ["vue3-base"], provides: ["vue"] }
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
writeFixturePackage(root, {
|
|
323
|
+
dir: "app/node_modules/vue",
|
|
324
|
+
packageJson: { name: "vue", version: "2.7.16" }
|
|
325
|
+
});
|
|
326
|
+
const { resolution, lowered } = lowerApp(appDir);
|
|
327
|
+
expect(lowered.imports?.vue).toBeUndefined();
|
|
328
|
+
expect(
|
|
329
|
+
resolution.diagnostics.filter((d) => d.severity === "error")
|
|
330
|
+
).toEqual([]);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Diagnostic, EsmxDeclaration } from './types';
|
|
2
|
+
/** Package facts the resolver needs, read from a package root directory. */
|
|
3
|
+
export interface PackageRecord {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
root: string;
|
|
7
|
+
private: boolean;
|
|
8
|
+
dependencies: Record<string, string>;
|
|
9
|
+
peerDependencies: Record<string, string>;
|
|
10
|
+
devDependencies: Record<string, string>;
|
|
11
|
+
declaration: EsmxDeclaration | null;
|
|
12
|
+
diagnostics: Diagnostic[];
|
|
13
|
+
}
|
|
14
|
+
export interface ReadDeclarationResult extends PackageRecord {
|
|
15
|
+
declaration: EsmxDeclaration;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads a package root's package.json into a PackageRecord, whether or not
|
|
19
|
+
* it carries an `esmx` declaration. Returns null when no package.json can
|
|
20
|
+
* be read.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readPackageRecord(packageDir: string): PackageRecord | null;
|
|
23
|
+
/**
|
|
24
|
+
* Reads a package's `esmx` declaration. Returns null when the package has
|
|
25
|
+
* no `esmx` field (legacy module) or no readable package.json. Schema
|
|
26
|
+
* violations are reported in `diagnostics` with the valid remainder kept.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readDeclaration(packageDir: string): ReadDeclarationResult | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { validateDeclaration } from "./schema.mjs";
|
|
4
|
+
function readJsonFile(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function toStringRecord(value) {
|
|
12
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
const result = {};
|
|
16
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
17
|
+
if (typeof entry === "string") {
|
|
18
|
+
result[key] = entry;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
export function readPackageRecord(packageDir) {
|
|
24
|
+
const json = readJsonFile(path.resolve(packageDir, "package.json"));
|
|
25
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const pkg = json;
|
|
29
|
+
const name = typeof pkg.name === "string" ? pkg.name : "";
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
let declaration = null;
|
|
32
|
+
if (pkg.esmx !== void 0) {
|
|
33
|
+
const validated = validateDeclaration(pkg.esmx, name || packageDir);
|
|
34
|
+
diagnostics.push(...validated.diagnostics);
|
|
35
|
+
declaration = validated.declaration ?? {};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
version: typeof pkg.version === "string" ? pkg.version : "",
|
|
40
|
+
root: packageDir,
|
|
41
|
+
private: pkg.private === true,
|
|
42
|
+
dependencies: toStringRecord(pkg.dependencies),
|
|
43
|
+
peerDependencies: toStringRecord(pkg.peerDependencies),
|
|
44
|
+
devDependencies: toStringRecord(pkg.devDependencies),
|
|
45
|
+
declaration,
|
|
46
|
+
diagnostics
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function readDeclaration(packageDir) {
|
|
50
|
+
const record = readPackageRecord(packageDir);
|
|
51
|
+
if (!record || record.declaration === null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return { ...record, declaration: record.declaration };
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { COMMAND, Esmx } from "../core.mjs";
|
|
5
|
+
import { MANIFEST_PROTOCOL_VERSION } from "../manifest-json.mjs";
|
|
6
|
+
import { createFixtureRoot, removeFixtureRoot } from "./test-fixtures.mjs";
|
|
7
|
+
const fixtureRoots = [];
|
|
8
|
+
async function fixtureRoot() {
|
|
9
|
+
const root = await createFixtureRoot();
|
|
10
|
+
fixtureRoots.push(root);
|
|
11
|
+
return root;
|
|
12
|
+
}
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
|
|
15
|
+
});
|
|
16
|
+
function writeModule(rootDir, dir, packageJson, manifest) {
|
|
17
|
+
const moduleDir = path.join(rootDir, dir);
|
|
18
|
+
fs.mkdirSync(moduleDir, { recursive: true });
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(moduleDir, "package.json"),
|
|
21
|
+
JSON.stringify(packageJson, null, 4)
|
|
22
|
+
);
|
|
23
|
+
const full = {
|
|
24
|
+
protocol: MANIFEST_PROTOCOL_VERSION,
|
|
25
|
+
name: String(packageJson.name),
|
|
26
|
+
version: String(packageJson.version ?? "0.0.0"),
|
|
27
|
+
provides: {},
|
|
28
|
+
uses: [],
|
|
29
|
+
scopes: {},
|
|
30
|
+
exports: {},
|
|
31
|
+
files: [],
|
|
32
|
+
chunks: {},
|
|
33
|
+
...manifest
|
|
34
|
+
};
|
|
35
|
+
for (const env of ["client", "server"]) {
|
|
36
|
+
const envDir = path.join(moduleDir, "dist", env);
|
|
37
|
+
fs.mkdirSync(envDir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(envDir, "manifest.json"),
|
|
40
|
+
JSON.stringify(full, null, 4)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return moduleDir;
|
|
44
|
+
}
|
|
45
|
+
function buildFixture(root, sharedExportFile) {
|
|
46
|
+
writeModule(
|
|
47
|
+
root,
|
|
48
|
+
"app/node_modules/shared",
|
|
49
|
+
{
|
|
50
|
+
name: "shared",
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
esmx: { exports: { "./ui": "./src/ui.ts" } }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
exports: {
|
|
56
|
+
ui: {
|
|
57
|
+
name: "ui",
|
|
58
|
+
pkg: false,
|
|
59
|
+
file: sharedExportFile,
|
|
60
|
+
identifier: "shared/ui"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
return writeModule(
|
|
66
|
+
root,
|
|
67
|
+
"app",
|
|
68
|
+
{
|
|
69
|
+
name: "app",
|
|
70
|
+
version: "1.0.0",
|
|
71
|
+
esmx: { uses: ["shared"] }
|
|
72
|
+
},
|
|
73
|
+
{}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function rewriteSharedExport(appDir, file) {
|
|
77
|
+
const manifestPath = path.join(
|
|
78
|
+
appDir,
|
|
79
|
+
"node_modules/shared/dist/client/manifest.json"
|
|
80
|
+
);
|
|
81
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
82
|
+
manifest.exports.ui.file = file;
|
|
83
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 4));
|
|
84
|
+
}
|
|
85
|
+
describe("Esmx.reinit generational relink (RFC 0001 \xA79)", () => {
|
|
86
|
+
it("adopts a republished remote on a new generation", async () => {
|
|
87
|
+
const root = await fixtureRoot();
|
|
88
|
+
const appDir = buildFixture(root, "ui.gen1.mjs");
|
|
89
|
+
const esmx = new Esmx({ root: appDir, isProd: true });
|
|
90
|
+
await esmx.init(COMMAND.preview);
|
|
91
|
+
const before = await esmx.getImportMap("client");
|
|
92
|
+
expect(before.imports?.["shared/ui"]).toBe("/shared/ui.gen1.mjs");
|
|
93
|
+
rewriteSharedExport(appDir, "ui.gen2.mjs");
|
|
94
|
+
const stillCached = await esmx.getImportMap("client");
|
|
95
|
+
expect(stillCached.imports?.["shared/ui"]).toBe("/shared/ui.gen1.mjs");
|
|
96
|
+
const ok = await esmx.reinit();
|
|
97
|
+
expect(ok).toBe(true);
|
|
98
|
+
const after = await esmx.getImportMap("client");
|
|
99
|
+
expect(after.imports?.["shared/ui"]).toBe("/shared/ui.gen2.mjs");
|
|
100
|
+
});
|
|
101
|
+
it("rolls back to the previous generation when the relink fails", async () => {
|
|
102
|
+
const root = await fixtureRoot();
|
|
103
|
+
const appDir = buildFixture(root, "ui.gen1.mjs");
|
|
104
|
+
const esmx = new Esmx({ root: appDir, isProd: true });
|
|
105
|
+
await esmx.init(COMMAND.preview);
|
|
106
|
+
const moduleConfigBefore = esmx.moduleConfig;
|
|
107
|
+
fs.writeFileSync(path.join(appDir, "package.json"), "{ not json");
|
|
108
|
+
await expect(esmx.reinit()).rejects.toThrow();
|
|
109
|
+
expect(esmx.moduleConfig).toBe(moduleConfigBefore);
|
|
110
|
+
const after = await esmx.getImportMap("client");
|
|
111
|
+
expect(after.imports?.["shared/ui"]).toBe("/shared/ui.gen1.mjs");
|
|
112
|
+
});
|
|
113
|
+
it("throws when called before init", async () => {
|
|
114
|
+
const esmx = new Esmx({ root: "/nonexistent" });
|
|
115
|
+
await expect(esmx.reinit()).rejects.toThrow();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReadDeclarationResult } from './reader';
|
|
2
|
+
import { type Diagnostic } from './types';
|
|
3
|
+
/** A resolved mount-table entry (RFC §6). */
|
|
4
|
+
export interface ResolvedMount {
|
|
5
|
+
name: string;
|
|
6
|
+
/** Package root directory (realpath'd for auto-mounts). */
|
|
7
|
+
root: string;
|
|
8
|
+
/** Artifact directory, `<root>/dist` for auto-mounts. */
|
|
9
|
+
artifactDir: string;
|
|
10
|
+
/** Whether `dist/client/manifest.json` exists. */
|
|
11
|
+
built: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Winner of the recursive supply merge for one (package, major) group
|
|
15
|
+
* (RFC §7, per-major amendment): elections are keyed by the MAJOR of the
|
|
16
|
+
* provider's resolved version, so coexisting majors are isolated islands,
|
|
17
|
+
* each with its own winner.
|
|
18
|
+
*/
|
|
19
|
+
export interface SupplyGroup {
|
|
20
|
+
/** Major of the group's resolved version; 'unknown' when unresolvable. */
|
|
21
|
+
major: number | 'unknown';
|
|
22
|
+
provider: string;
|
|
23
|
+
/** Provider's resolved installed version, null when unresolvable. */
|
|
24
|
+
version: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** Per-package election result: one winner per major group. */
|
|
27
|
+
export interface SupplyEntry {
|
|
28
|
+
/** Group winners, highest major first ('unknown' last). */
|
|
29
|
+
groups: SupplyGroup[];
|
|
30
|
+
}
|
|
31
|
+
export interface ResolveMountsResult {
|
|
32
|
+
supply: Record<string, SupplyEntry>;
|
|
33
|
+
mounts: Record<string, ResolvedMount>;
|
|
34
|
+
diagnostics: Diagnostic[];
|
|
35
|
+
}
|
|
36
|
+
export interface ResolverEnvLinks {
|
|
37
|
+
[moduleName: string]: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Consumer-side group selection (RFC §7, per-major amendment): the group
|
|
41
|
+
* whose winner satisfies `range`; multiple satisfying groups → highest
|
|
42
|
+
* major; no range (or a workspace placeholder) → highest major group;
|
|
43
|
+
* range satisfied by no group → highest major group (the caller diagnoses
|
|
44
|
+
* the violation as E_VERSION). Groups whose version cannot be checked
|
|
45
|
+
* against the range (unparsable either side) rank after satisfying groups
|
|
46
|
+
* but before violating ones, per the RFC §11 skip-the-gate rule.
|
|
47
|
+
*/
|
|
48
|
+
export declare function selectSupplyGroup(entry: SupplyEntry, range?: string): SupplyGroup | null;
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the mount table and the recursive supply merge for a root
|
|
51
|
+
* module's declaration (RFC §6 + §7 phases 1–2).
|
|
52
|
+
*
|
|
53
|
+
* - `envLinks` entries override auto-mounting; their values follow today's
|
|
54
|
+
* `ModuleConfig.links` semantics (artifact directory, resolved relative
|
|
55
|
+
* to `rootDir`).
|
|
56
|
+
* - Auto-mounts resolve via Node resolution from the DECLARING module's
|
|
57
|
+
* own location and are realpath'd once.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveMounts(rootDir: string, rootPackage: ReadDeclarationResult, envLinks?: ResolverEnvLinks): ResolveMountsResult;
|