@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,430 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { MANIFEST_PROTOCOL_VERSION } from "../manifest-json.mjs";
4
+ import { readPackageRecord } from "./reader.mjs";
5
+ import { parseSemver, satisfiesRange } from "./semver.mjs";
6
+ import { DiagnosticCode } from "./types.mjs";
7
+ function resolvePackageDir(fromDir, name) {
8
+ let dir = path.resolve(fromDir);
9
+ for (; ; ) {
10
+ const candidate = path.join(dir, "node_modules", name);
11
+ if (fs.existsSync(path.join(candidate, "package.json"))) {
12
+ return candidate;
13
+ }
14
+ const parent = path.dirname(dir);
15
+ if (parent === dir) {
16
+ return null;
17
+ }
18
+ dir = parent;
19
+ }
20
+ }
21
+ function resolveInstalledVersion(fromDir, packageName) {
22
+ const packageDir = resolvePackageDir(fromDir, packageName);
23
+ if (!packageDir) {
24
+ return null;
25
+ }
26
+ const record = readPackageRecord(packageDir);
27
+ return record && record.version !== "" ? record.version : null;
28
+ }
29
+ function readManifestInfo(artifactDir) {
30
+ let json;
31
+ try {
32
+ json = JSON.parse(
33
+ fs.readFileSync(
34
+ path.join(artifactDir, "client/manifest.json"),
35
+ "utf-8"
36
+ )
37
+ );
38
+ } catch {
39
+ return null;
40
+ }
41
+ if (typeof json !== "object" || json === null) {
42
+ return null;
43
+ }
44
+ const manifest = json;
45
+ const protocol = typeof manifest.protocol === "number" ? manifest.protocol : 1;
46
+ return { protocol };
47
+ }
48
+ function isWorkspacePlaceholderRange(range) {
49
+ return /^(workspace|file|link|portal):/.test(range);
50
+ }
51
+ function majorKeyOf(version) {
52
+ const parsed = version ? parseSemver(version) : null;
53
+ return parsed ? String(parsed.major) : "unknown";
54
+ }
55
+ function sortedMajorKeys(groups) {
56
+ return Object.keys(groups).sort((a, b) => {
57
+ if (a === "unknown") {
58
+ return 1;
59
+ }
60
+ if (b === "unknown") {
61
+ return -1;
62
+ }
63
+ return Number(b) - Number(a);
64
+ });
65
+ }
66
+ function mergeGrouped(into, from, onConflict) {
67
+ for (const [packageName, groups] of Object.entries(from)) {
68
+ const target = into[packageName] ?? (into[packageName] = {});
69
+ for (const [majorKey, winner] of Object.entries(groups)) {
70
+ const existing = target[majorKey];
71
+ if (existing) {
72
+ if (existing.provider !== winner.provider) {
73
+ onConflict(
74
+ packageName,
75
+ majorKey,
76
+ existing.provider,
77
+ winner.provider
78
+ );
79
+ }
80
+ continue;
81
+ }
82
+ target[majorKey] = winner;
83
+ }
84
+ }
85
+ }
86
+ export function selectSupplyGroup(entry, range) {
87
+ if (entry.groups.length === 0) {
88
+ return null;
89
+ }
90
+ if (range === void 0 || isWorkspacePlaceholderRange(range)) {
91
+ return entry.groups[0];
92
+ }
93
+ let unknownFallback = null;
94
+ for (const group of entry.groups) {
95
+ const satisfied = group.version ? satisfiesRange(group.version, range) : null;
96
+ if (satisfied === true) {
97
+ return group;
98
+ }
99
+ if (satisfied === null && unknownFallback === null) {
100
+ unknownFallback = group;
101
+ }
102
+ }
103
+ return unknownFallback ?? entry.groups[0];
104
+ }
105
+ export function resolveMounts(rootDir, rootPackage, envLinks) {
106
+ const diagnostics = [...rootPackage.diagnostics];
107
+ const mounts = {};
108
+ const records = /* @__PURE__ */ new Map([
109
+ [rootPackage.name, rootPackage]
110
+ ]);
111
+ const supplyMemo = /* @__PURE__ */ new Map();
112
+ const usedSupplyMemo = /* @__PURE__ */ new Map();
113
+ const visiting = /* @__PURE__ */ new Set();
114
+ let cyclic = false;
115
+ const reportedDupes = /* @__PURE__ */ new Set();
116
+ function reportDupProvider(consumer, packageName, majorKey, existing, incoming) {
117
+ const key = `${packageName}@${majorKey}`;
118
+ if (reportedDupes.has(key)) {
119
+ return;
120
+ }
121
+ reportedDupes.add(key);
122
+ diagnostics.push({
123
+ code: DiagnosticCode.E_DUP_PROVIDER,
124
+ severity: "error",
125
+ module: consumer,
126
+ package: packageName,
127
+ found: `${existing}, ${incoming}`,
128
+ message: `Package "${packageName}" (major ${majorKey}) is provided by both "${existing}" and "${incoming}" in the closure of "${consumer}" \u2014 a shared dependency must have a single owner.`,
129
+ fix: `Consolidate "${packageName}" into one shared module that both consume via "uses", or give one copy a distinct package identity (an npm alias provided under its own name) if same-major coexistence is intended.`
130
+ });
131
+ }
132
+ function mountUsed(name, consumer) {
133
+ const known = records.get(name);
134
+ if (known) {
135
+ return known;
136
+ }
137
+ let packageRoot = null;
138
+ let artifactDir = null;
139
+ const envLink = envLinks?.[name];
140
+ if (envLink !== void 0) {
141
+ artifactDir = path.isAbsolute(envLink) ? envLink : path.resolve(rootDir, envLink);
142
+ packageRoot = path.dirname(artifactDir);
143
+ } else {
144
+ const resolved = resolvePackageDir(consumer.root, name);
145
+ if (resolved) {
146
+ packageRoot = fs.realpathSync(resolved);
147
+ artifactDir = path.join(packageRoot, "dist");
148
+ }
149
+ }
150
+ const record = packageRoot ? readPackageRecord(packageRoot) : null;
151
+ if (!record || !artifactDir || !packageRoot) {
152
+ diagnostics.push({
153
+ code: DiagnosticCode.E_NOT_LINKED,
154
+ severity: "error",
155
+ module: consumer.name,
156
+ package: name,
157
+ message: `Module "${consumer.name}" uses "${name}" but it is absent from the mount table (not installed and no explicit link).`,
158
+ fix: `Install "${name}" into node_modules, or add an explicit links entry pointing at its artifact directory.`
159
+ });
160
+ return null;
161
+ }
162
+ diagnostics.push(...record.diagnostics);
163
+ const built = fs.existsSync(
164
+ path.join(artifactDir, "client/manifest.json")
165
+ );
166
+ if (!built) {
167
+ diagnostics.push({
168
+ code: DiagnosticCode.E_NOT_BUILT,
169
+ severity: "error",
170
+ module: consumer.name,
171
+ package: name,
172
+ message: `Module "${name}" is mounted at ${artifactDir} but has no built artifact (dist/client/manifest.json missing). Manifest-dependent checks are skipped.`,
173
+ fix: `Build "${name}" first, then rebuild "${consumer.name}".`
174
+ });
175
+ } else {
176
+ const info = readManifestInfo(artifactDir);
177
+ if (info && info.protocol > MANIFEST_PROTOCOL_VERSION) {
178
+ diagnostics.push({
179
+ code: DiagnosticCode.E_PROTOCOL,
180
+ severity: "error",
181
+ module: consumer.name,
182
+ package: name,
183
+ found: String(info.protocol),
184
+ required: `<= ${MANIFEST_PROTOCOL_VERSION}`,
185
+ message: `Module "${name}" was built with manifest protocol ${info.protocol}, but this linker supports up to ${MANIFEST_PROTOCOL_VERSION}.`,
186
+ fix: `Upgrade esmx in "${consumer.name}", or rebuild "${name}" with a toolchain emitting protocol <= ${MANIFEST_PROTOCOL_VERSION}.`
187
+ });
188
+ }
189
+ }
190
+ mounts[name] = { name, root: packageRoot, artifactDir, built };
191
+ records.set(name, record);
192
+ return record;
193
+ }
194
+ function checkUsedVersion(consumer, used) {
195
+ const range = consumer.dependencies[used.name] ?? consumer.peerDependencies[used.name] ?? consumer.devDependencies[used.name];
196
+ if (range === void 0 || isWorkspacePlaceholderRange(range)) {
197
+ return;
198
+ }
199
+ if (used.private) {
200
+ return;
201
+ }
202
+ const satisfied = satisfiesRange(used.version, range);
203
+ if (satisfied === false) {
204
+ diagnostics.push({
205
+ code: DiagnosticCode.E_VERSION,
206
+ severity: "error",
207
+ module: consumer.name,
208
+ package: used.name,
209
+ check: "intent",
210
+ found: used.version,
211
+ required: range,
212
+ message: `Module "${consumer.name}" requires "${used.name}@${range}" but the mounted module is version ${used.version}.`,
213
+ fix: `Update the "${used.name}" range in "${consumer.name}" dependencies, or mount a matching version.`
214
+ });
215
+ }
216
+ }
217
+ function supplyOf(record) {
218
+ const memoized = supplyMemo.get(record.name);
219
+ if (memoized) {
220
+ return memoized;
221
+ }
222
+ if (visiting.has(record.name)) {
223
+ cyclic = true;
224
+ diagnostics.push({
225
+ code: DiagnosticCode.E_CYCLE,
226
+ severity: "error",
227
+ module: record.name,
228
+ message: `The uses chain revisits module "${record.name}" \u2014 a uses cycle is an architecture error.`,
229
+ fix: `Break the cycle by removing one of the "uses" edges that closes it.`
230
+ });
231
+ return {};
232
+ }
233
+ visiting.add(record.name);
234
+ const onConflict = (packageName, majorKey, existing, incoming) => reportDupProvider(
235
+ record.name,
236
+ packageName,
237
+ majorKey,
238
+ existing,
239
+ incoming
240
+ );
241
+ const fromUses = {};
242
+ for (const usedName of record.declaration?.uses ?? []) {
243
+ const used = mountUsed(usedName, record);
244
+ if (!used) {
245
+ continue;
246
+ }
247
+ checkUsedVersion(record, used);
248
+ mergeGrouped(fromUses, supplyOf(used), onConflict);
249
+ }
250
+ usedSupplyMemo.set(record.name, fromUses);
251
+ const merged = {};
252
+ mergeGrouped(merged, fromUses, onConflict);
253
+ for (const provided of record.declaration?.provides ?? []) {
254
+ const version = resolveInstalledVersion(record.root, provided);
255
+ const majorKey = majorKeyOf(version);
256
+ const existing = merged[provided]?.[majorKey];
257
+ if (existing && existing.provider !== record.name) {
258
+ reportDupProvider(
259
+ record.name,
260
+ provided,
261
+ majorKey,
262
+ existing.provider,
263
+ record.name
264
+ );
265
+ continue;
266
+ }
267
+ merged[provided] = {
268
+ ...merged[provided],
269
+ [majorKey]: { provider: record.name, version }
270
+ };
271
+ }
272
+ visiting.delete(record.name);
273
+ supplyMemo.set(record.name, merged);
274
+ return merged;
275
+ }
276
+ const targetChecks = [];
277
+ const rootEntry = rootPackage.declaration.entry;
278
+ if (rootEntry?.client) {
279
+ targetChecks.push(["entry.client", rootEntry.client]);
280
+ }
281
+ if (rootEntry?.server) {
282
+ targetChecks.push(["entry.server", rootEntry.server]);
283
+ }
284
+ for (const [name, value] of Object.entries(
285
+ rootPackage.declaration.exports ?? {}
286
+ )) {
287
+ if (typeof value === "string") {
288
+ targetChecks.push([`exports['${name}']`, value]);
289
+ } else {
290
+ if (typeof value.client === "string") {
291
+ targetChecks.push([`exports['${name}'].client`, value.client]);
292
+ }
293
+ if (typeof value.server === "string") {
294
+ targetChecks.push([`exports['${name}'].server`, value.server]);
295
+ }
296
+ }
297
+ }
298
+ for (const [label, relativePath] of targetChecks) {
299
+ if (!fs.existsSync(path.resolve(rootPackage.root, relativePath))) {
300
+ diagnostics.push({
301
+ code: DiagnosticCode.E_TARGET_MISSING,
302
+ severity: "error",
303
+ module: rootPackage.name,
304
+ found: relativePath,
305
+ message: `Declared ${label} target "${relativePath}" does not exist in module "${rootPackage.name}".`,
306
+ fix: `Create the file, or fix the path in the "esmx" declaration.`
307
+ });
308
+ }
309
+ }
310
+ const groupedSupply = supplyOf(rootPackage);
311
+ if (cyclic) {
312
+ return { supply: {}, mounts, diagnostics };
313
+ }
314
+ const supply = {};
315
+ for (const [packageName, groups] of Object.entries(groupedSupply)) {
316
+ supply[packageName] = {
317
+ groups: sortedMajorKeys(groups).map((majorKey) => ({
318
+ major: majorKey === "unknown" ? "unknown" : Number(majorKey),
319
+ provider: groups[majorKey].provider,
320
+ version: groups[majorKey].version
321
+ }))
322
+ };
323
+ }
324
+ for (const [packageName, entry] of Object.entries(supply)) {
325
+ if (entry.groups.length < 2) {
326
+ continue;
327
+ }
328
+ const summary = entry.groups.map(
329
+ (group) => `${group.major} \u2192 "${group.provider}"@${group.version ?? "unresolved"}`
330
+ ).join(", ");
331
+ diagnostics.push({
332
+ code: DiagnosticCode.W_MULTI_MAJOR,
333
+ severity: "warning",
334
+ module: rootPackage.name,
335
+ package: packageName,
336
+ found: summary,
337
+ message: `Package "${packageName}" has coexisting major versions: ${summary}. Each major is an isolated island with its own winner; every consumer wires to the group satisfying its own range.`,
338
+ fix: `No action needed if coexistence is intended. Otherwise align the providers' installed "${packageName}" majors.`
339
+ });
340
+ }
341
+ function wiredGroupFor(record, packageName, entry) {
342
+ if (record.declaration?.provides?.includes(packageName)) {
343
+ const selfKey = majorKeyOf(
344
+ resolveInstalledVersion(record.root, packageName)
345
+ );
346
+ const own = entry.groups.find(
347
+ (group) => String(group.major) === selfKey
348
+ );
349
+ if (own) {
350
+ return own;
351
+ }
352
+ }
353
+ const range = record.dependencies[packageName] ?? record.peerDependencies[packageName];
354
+ return selectSupplyGroup(entry, range);
355
+ }
356
+ for (const record of records.values()) {
357
+ for (const [packageName, entry] of Object.entries(supply)) {
358
+ const range = record.dependencies[packageName] ?? record.peerDependencies[packageName];
359
+ if (range === void 0 || isWorkspacePlaceholderRange(range)) {
360
+ continue;
361
+ }
362
+ const group = wiredGroupFor(record, packageName, entry);
363
+ if (!group?.version) {
364
+ continue;
365
+ }
366
+ const satisfied = satisfiesRange(group.version, range);
367
+ if (satisfied === false) {
368
+ diagnostics.push({
369
+ code: DiagnosticCode.E_VERSION,
370
+ severity: "error",
371
+ module: record.name,
372
+ package: packageName,
373
+ check: "intent",
374
+ found: group.version,
375
+ required: range,
376
+ message: `Layer "${record.name}" declares "${packageName}@${range}" but its wired group winner "${group.provider}" resolves ${group.version}.`,
377
+ fix: `Update the "${packageName}" range in "${record.name}", or change the winning provider's installed version.`
378
+ });
379
+ }
380
+ }
381
+ }
382
+ for (const record of records.values()) {
383
+ const usedSupply = usedSupplyMemo.get(record.name);
384
+ if (!usedSupply) {
385
+ continue;
386
+ }
387
+ for (const packageName of Object.keys(usedSupply)) {
388
+ const hasRange = packageName in record.dependencies || packageName in record.peerDependencies || packageName in record.devDependencies;
389
+ if (hasRange) {
390
+ continue;
391
+ }
392
+ diagnostics.push({
393
+ code: DiagnosticCode.W_NO_RANGE,
394
+ severity: "warning",
395
+ module: record.name,
396
+ package: packageName,
397
+ message: `Layer "${record.name}" receives "${packageName}" from its uses chain but declares no version range for it.`,
398
+ fix: `Add "${packageName}" to "${record.name}" dependencies or peerDependencies with the intended range.`
399
+ });
400
+ }
401
+ }
402
+ for (const record of records.values()) {
403
+ for (const [packageName, entry] of Object.entries(supply)) {
404
+ if (!(packageName in record.devDependencies)) {
405
+ continue;
406
+ }
407
+ const group = wiredGroupFor(record, packageName, entry);
408
+ if (!group?.version) {
409
+ continue;
410
+ }
411
+ const localVersion = resolveInstalledVersion(
412
+ record.root,
413
+ packageName
414
+ );
415
+ if (localVersion && localVersion !== group.version) {
416
+ diagnostics.push({
417
+ code: DiagnosticCode.W_TYPE_DRIFT,
418
+ severity: "warning",
419
+ module: record.name,
420
+ package: packageName,
421
+ found: localVersion,
422
+ required: group.version,
423
+ message: `Layer "${record.name}" types against its local "${packageName}@${localVersion}" but the code runs on the wired group winner's ${group.version}.`,
424
+ fix: `Align the "${packageName}" devDependencies copy in "${record.name}" with the winner's version ${group.version}.`
425
+ });
426
+ }
427
+ }
428
+ }
429
+ return { supply, mounts, diagnostics };
430
+ }
@@ -0,0 +1 @@
1
+ export {};