@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
package/README.md CHANGED
@@ -29,10 +29,10 @@
29
29
 
30
30
  ## ๐Ÿš€ Features
31
31
 
32
- - **Native Microfrontend** - Based on ESM + ImportMap for native module loading with zero runtime overhead
32
+ - **Native Microfrontend** - Based on ESM + Import Map for native module loading with zero runtime overhead
33
33
  - **Dependency Management** - Centralized dependency management with content-hash-based strong caching strategy
34
34
  - **Application Isolation** - ESM native module isolation ensuring runtime stability
35
- - **SSR Support** - Flexible server-side rendering strategy supporting any frontend framework
35
+ - **SSR Support** - Flexible SSR strategy supporting any frontend framework
36
36
  - **Developer Experience** - Complete TypeScript support with native Module Linking capabilities
37
37
 
38
38
  ## ๐Ÿ“ฆ Installation
@@ -50,7 +50,21 @@ yarn add @esmx/core
50
50
 
51
51
  ## ๐Ÿš€ Quick Start
52
52
 
53
- See Quick Start guide: https://esmx.dev/guide/start/getting-started.html
53
+ Define your app in `entry.node.ts`, choosing a build-tool integration in `devApp`:
54
+
55
+ ```typescript
56
+ import type { EsmxOptions } from '@esmx/core';
57
+
58
+ export default {
59
+ async devApp(esmx) {
60
+ return import('@esmx/rspack').then((m) => m.createRspackHtmlApp(esmx));
61
+ }
62
+ } satisfies EsmxOptions;
63
+ ```
64
+
65
+ Then drive it with the `esmx` CLI: `esmx dev`, `esmx build`, `esmx start`.
66
+
67
+ ๐Ÿ“– [Documentation](https://esmx.dev/guide/start/getting-started.html)
54
68
 
55
69
  ## ๐Ÿ“š Documentation
56
70
 
package/README.zh-CN.md CHANGED
@@ -20,7 +20,7 @@
20
20
  </a>
21
21
  </div>
22
22
 
23
- <p>ๆ”ฏๆŒ Vueใ€Reactใ€Preactใ€Solidใ€Svelte ็š„้ซ˜ๆ€ง่ƒฝๅพฎๅ‰็ซฏๆก†ๆžถ๏ผŒๅ…ทๅค‡ SSR ๅ’Œๆจกๅ—้“พๆŽฅ่ƒฝๅŠ›</p>
23
+ <p>ๆ”ฏๆŒ Vueใ€Reactใ€Preactใ€Solidใ€Svelte ็š„้ซ˜ๆ€ง่ƒฝๅพฎๅ‰็ซฏๆก†ๆžถ๏ผŒๆ”ฏๆŒ SSR ไธŽๆจกๅ—้“พๆŽฅ</p>
24
24
 
25
25
  <p>
26
26
  <a href="https://github.com/esmnext/esmx/blob/master/packages/core/README.md">English</a> | ไธญๆ–‡
@@ -29,10 +29,10 @@
29
29
 
30
30
  ## ๐Ÿš€ ็‰นๆ€ง
31
31
 
32
- - **ๅŽŸ็”Ÿๅพฎๅ‰็ซฏ** - ๅŸบไบŽ ESM + ImportMap ็š„ๅŽŸ็”Ÿๆจกๅ—ๅŠ ่ฝฝ๏ผŒ้›ถ่ฟ่กŒๆ—ถๅผ€้”€
33
- - **ไพ่ต–็ฎก็†** - ไธญๅฟƒๅŒ–็š„ไพ่ต–็ฎก็†๏ผŒๅŸบไบŽๅ†…ๅฎนๅ“ˆๅธŒ็š„ๅผบ็ผ“ๅญ˜็ญ–็•ฅ
34
- - **ๅบ”็”จ้š”็ฆป** - ESM ๅŽŸ็”Ÿๆจกๅ—้š”็ฆป๏ผŒไฟ้šœๅบ”็”จ่ฟ่กŒๆ—ถ็š„็จณๅฎšๆ€ง
35
- - **SSR ๆ”ฏๆŒ** - ็ตๆดป็š„ๆœๅŠก็ซฏๆธฒๆŸ“็ญ–็•ฅ๏ผŒๆ”ฏๆŒไปปๆ„ๅ‰็ซฏๆก†ๆžถ
32
+ - **ๅŽŸ็”Ÿๅพฎๅ‰็ซฏ** - ๅŸบไบŽ ESM + Import Map ็š„ๅŽŸ็”Ÿๆจกๅ—ๅŠ ่ฝฝ๏ผŒ้›ถ่ฟ่กŒๆ—ถๅผ€้”€
33
+ - **ไพ่ต–็ฎก็†** - ไธญๅฟƒๅŒ–ไพ่ต–็ฎก็†๏ผŒๅŸบไบŽๅ†…ๅฎนๅ“ˆๅธŒ็š„ๅผบ็ผ“ๅญ˜็ญ–็•ฅ
34
+ - **ๅบ”็”จ้š”็ฆป** - ESM ๅŽŸ็”Ÿๆจกๅ—้š”็ฆป๏ผŒไฟ้šœๅบ”็”จ่ฟ่กŒๆ—ถ็จณๅฎšๆ€ง
35
+ - **SSR ๆ”ฏๆŒ** - ็ตๆดป็š„ SSR ็ญ–็•ฅ๏ผŒๆ”ฏๆŒไปปๆ„ๅ‰็ซฏๆก†ๆžถ
36
36
  - **ๅผ€ๅ‘ไฝ“้ชŒ** - ๅฎŒๆ•ด็š„ TypeScript ๆ”ฏๆŒ๏ผŒๅŽŸ็”Ÿๆจกๅ—้“พๆŽฅ่ƒฝๅŠ›
37
37
 
38
38
  ## ๐Ÿ“ฆ ๅฎ‰่ฃ…
@@ -50,7 +50,21 @@ yarn add @esmx/core
50
50
 
51
51
  ## ๐Ÿš€ ๅฟซ้€Ÿๅผ€ๅง‹
52
52
 
53
- ๆŸฅ็œ‹ๅฟซ้€Ÿๅผ€ๅง‹ๆŒ‡ๅ—๏ผšhttps://esmx.dev/zh-CN/guide/start/getting-started.html
53
+ ๅœจ `entry.node.ts` ไธญๅฎšไน‰ๅบ”็”จ๏ผŒๅนถๅœจ `devApp` ้€‰ๆ‹ฉๆž„ๅปบๅทฅๅ…ท้›†ๆˆ๏ผš
54
+
55
+ ```typescript
56
+ import type { EsmxOptions } from '@esmx/core';
57
+
58
+ export default {
59
+ async devApp(esmx) {
60
+ return import('@esmx/rspack').then((m) => m.createRspackHtmlApp(esmx));
61
+ }
62
+ } satisfies EsmxOptions;
63
+ ```
64
+
65
+ ็„ถๅŽ็”จ `esmx` CLI ้ฉฑๅŠจ๏ผš`esmx dev`ใ€`esmx build`ใ€`esmx start`ใ€‚
66
+
67
+ ๐Ÿ“– [ๅฎŒๆ•ดๆ–‡ๆกฃ](https://esmx.dev/zh/guide/start/getting-started.html)
54
68
 
55
69
  ## ๐Ÿ“š ๆ–‡ๆกฃ
56
70
 
package/dist/app.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { pathToFileURL } from "node:url";
2
2
  import { createLoaderImport } from "@esmx/import";
3
+ import { DEFAULT_MODULE_ENTRY } from "./module-config.mjs";
3
4
  import {
4
5
  RenderContext
5
6
  } from "./render-context.mjs";
@@ -15,9 +16,10 @@ async function createStartRender(esmx) {
15
16
  const baseURL = pathToFileURL(esmx.resolvePath("dist/server"));
16
17
  const importMap = await esmx.getImportMap("server");
17
18
  const loaderImport = createLoaderImport(baseURL, importMap);
19
+ const serverEntry = esmx.moduleConfig.entry.server ?? DEFAULT_MODULE_ENTRY.server;
18
20
  return async (options) => {
19
21
  const rc = new RenderContext(esmx, options);
20
- const result = await loaderImport(`${esmx.name}/src/entry.server`);
22
+ const result = await loaderImport(`${esmx.name}/${serverEntry.name}`);
21
23
  const serverRender = result[rc.entryName];
22
24
  if (typeof serverRender === "function") {
23
25
  await serverRender(rc);
@@ -28,7 +30,7 @@ async function createStartRender(esmx) {
28
30
  function createErrorRender(esmx) {
29
31
  return (options) => {
30
32
  throw new Error(
31
- `App instance is only available in production and can only execute built artifacts.`
33
+ `[@esmx/core] esmx.render() is only available in production. The current process is in dev mode and has no built artifacts. Run \`esmx build\` first, then start the server with NODE_ENV=production node dist/index.mjs.`
32
34
  );
33
35
  };
34
36
  }
package/dist/cli/cli.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export declare function cli(command: string): Promise<void>;
2
2
  export declare function resolve(specifier: string, context: Record<string, any>, nextResolve: Function): any;
3
+ export declare function load(url: string, context: Record<string, any>, nextLoad: Function): Promise<any>;
package/dist/cli/cli.mjs CHANGED
@@ -3,6 +3,7 @@ import { styleText } from "node:util";
3
3
  import pkg from "../../package.json" with { type: "json" };
4
4
  import { COMMAND, Esmx } from "../core.mjs";
5
5
  import { resolveImportPath } from "../utils/resolve-path.mjs";
6
+ import { runValidate, VALIDATE_HELP } from "./validate.mjs";
6
7
  async function getSrcOptions() {
7
8
  return import(resolveImportPath(process.cwd(), "./src/entry.node.ts")).then(
8
9
  (m) => m.default
@@ -24,6 +25,21 @@ async function getDistOptions() {
24
25
  }
25
26
  }
26
27
  export async function cli(command) {
28
+ if (command === "validate") {
29
+ const flags = process.argv.slice(3);
30
+ if (flags.includes("--help")) {
31
+ console.log(VALIDATE_HELP);
32
+ return;
33
+ }
34
+ const result = await runValidate(process.cwd(), {
35
+ json: flags.includes("--json")
36
+ });
37
+ console.log(result.output);
38
+ if (result.exitCode !== 0) {
39
+ process.exit(result.exitCode);
40
+ }
41
+ return;
42
+ }
27
43
  console.log(`\u{1F525} ${styleText("yellow", "Esmx")} v${pkg.version}
28
44
  `);
29
45
  if (command !== COMMAND.dev && typeof process.env.NODE_ENV === "undefined") {
@@ -87,8 +103,19 @@ module.register(import.meta.url, {
87
103
  parentURL: import.meta.url
88
104
  });
89
105
  export function resolve(specifier, context, nextResolve) {
90
- if (context?.parentURL.endsWith(".ts") && specifier.startsWith(".") && !specifier.endsWith(".ts")) {
106
+ if (context?.parentURL?.endsWith(".ts") && specifier.startsWith(".") && !specifier.endsWith(".ts")) {
91
107
  return nextResolve(specifier + ".ts", context);
92
108
  }
93
109
  return nextResolve(specifier, context);
94
110
  }
111
+ const STYLE_ASSET_RE = /\.(?:css|scss|sass|less|stylus|styl|pcss|postcss)(?:\?.*)?$/i;
112
+ export async function load(url, context, nextLoad) {
113
+ if (STYLE_ASSET_RE.test(url)) {
114
+ return {
115
+ format: "module",
116
+ shortCircuit: true,
117
+ source: `export default ${JSON.stringify(url)}; export const href = ${JSON.stringify(url)};`
118
+ };
119
+ }
120
+ return nextLoad(url, context);
121
+ }
@@ -0,0 +1,15 @@
1
+ export declare const VALIDATE_HELP = "Usage: esmx validate [--json]\n\nBuild-free dry run of the RFC 0001 module protocol: reads the package.json\n\"esmx\" declaration in the current directory and runs the resolution layer\n(phases 1-2: consumption graph + supply merge) without building anything.\n\nExit code is non-zero only when an error-severity diagnostic is found;\nwarnings alone exit 0. A package without an \"esmx\" field uses the legacy\nentry.node.ts config and is not an error.\n\nOptions:\n --json Emit a machine-readable envelope on stdout and nothing else:\n {\n \"diagnostics\": [ { \"code\", \"check\"?, \"module\", \"package\"?,\n \"found\"?, \"required\"?, \"message\", \"fix\" } ],\n \"supply\": { \"<package>\": { \"groups\": [ { \"major\",\n \"provider\", \"version\" } ] } },\n \"mounts\": { \"<module>\": { \"name\", \"root\",\n \"artifactDir\", \"built\" } }\n }\n Two providers of one (package, major) is an E_DUP_PROVIDER error\n (a shared dependency must have a single owner).\n Legacy packages emit { \"protocol\": \"legacy\", \"diagnostics\": [] }.\n --help Show this message.";
2
+ export interface RunValidateOptions {
3
+ json?: boolean;
4
+ }
5
+ export interface RunValidateResult {
6
+ exitCode: number;
7
+ /** Full stdout payload (JSON envelope or human report). */
8
+ output: string;
9
+ }
10
+ /**
11
+ * `esmx validate` core (RFC 0001 ยง10 Phase 5, the agent verification loop).
12
+ * Pure with respect to process state: never writes to stdout/stderr and
13
+ * never exits โ€” the CLI case owns printing and the exit code.
14
+ */
15
+ export declare function runValidate(rootDir: string, options?: RunValidateOptions): Promise<RunValidateResult>;
@@ -0,0 +1,139 @@
1
+ import { styleText } from "node:util";
2
+ import { resolveDeclaration } from "../declaration/index.mjs";
3
+ export const VALIDATE_HELP = `Usage: esmx validate [--json]
4
+
5
+ Build-free dry run of the RFC 0001 module protocol: reads the package.json
6
+ "esmx" declaration in the current directory and runs the resolution layer
7
+ (phases 1-2: consumption graph + supply merge) without building anything.
8
+
9
+ Exit code is non-zero only when an error-severity diagnostic is found;
10
+ warnings alone exit 0. A package without an "esmx" field uses the legacy
11
+ entry.node.ts config and is not an error.
12
+
13
+ Options:
14
+ --json Emit a machine-readable envelope on stdout and nothing else:
15
+ {
16
+ "diagnostics": [ { "code", "check"?, "module", "package"?,
17
+ "found"?, "required"?, "message", "fix" } ],
18
+ "supply": { "<package>": { "groups": [ { "major",
19
+ "provider", "version" } ] } },
20
+ "mounts": { "<module>": { "name", "root",
21
+ "artifactDir", "built" } }
22
+ }
23
+ Two providers of one (package, major) is an E_DUP_PROVIDER error
24
+ (a shared dependency must have a single owner).
25
+ Legacy packages emit { "protocol": "legacy", "diagnostics": [] }.
26
+ --help Show this message.`;
27
+ function toEnvelopeEntry(diagnostic) {
28
+ const entry = {
29
+ code: diagnostic.code,
30
+ module: diagnostic.module,
31
+ message: diagnostic.message,
32
+ fix: diagnostic.fix
33
+ };
34
+ if (diagnostic.check !== void 0) {
35
+ entry.check = diagnostic.check;
36
+ }
37
+ if (diagnostic.package !== void 0) {
38
+ entry.package = diagnostic.package;
39
+ }
40
+ if (diagnostic.found !== void 0) {
41
+ entry.found = diagnostic.found;
42
+ }
43
+ if (diagnostic.required !== void 0) {
44
+ entry.required = diagnostic.required;
45
+ }
46
+ return entry;
47
+ }
48
+ function formatHumanDiagnostic(diagnostic) {
49
+ const color = diagnostic.severity === "error" ? "red" : "yellow";
50
+ const location = diagnostic.package ? `${diagnostic.module} \u2192 ${diagnostic.package}` : diagnostic.module;
51
+ const lines = [styleText(color, `[${diagnostic.code}]`) + ` ${location}`];
52
+ if (diagnostic.check) {
53
+ lines.push(` check: ${diagnostic.check}`);
54
+ }
55
+ if (diagnostic.found !== void 0 || diagnostic.required !== void 0) {
56
+ lines.push(
57
+ ` found: ${diagnostic.found ?? "\u2014"} / required: ${diagnostic.required ?? "\u2014"}`
58
+ );
59
+ }
60
+ lines.push(` ${diagnostic.message}`);
61
+ lines.push(` fix: ${diagnostic.fix}`);
62
+ return lines.join("\n");
63
+ }
64
+ function formatHumanReport(diagnostics, supply, mounts) {
65
+ const sections = [];
66
+ const errorCount = diagnostics.filter((d) => d.severity === "error").length;
67
+ const warningCount = diagnostics.length - errorCount;
68
+ if (diagnostics.length === 0) {
69
+ sections.push(styleText("green", "\u2713 Declaration is valid."));
70
+ } else {
71
+ sections.push(diagnostics.map(formatHumanDiagnostic).join("\n"));
72
+ sections.push(`${errorCount} error(s), ${warningCount} warning(s).`);
73
+ }
74
+ const supplyEntries = Object.entries(supply);
75
+ sections.push(
76
+ supplyEntries.length === 0 ? "Supply: (empty)" : [
77
+ "Supply:",
78
+ ...supplyEntries.flatMap(
79
+ ([pkg, entry]) => entry.groups.map(
80
+ (group) => ` ${pkg} \u2192 ${group.provider}@${group.version ?? "unresolved"}${entry.groups.length > 1 ? ` (major ${group.major})` : ""}`
81
+ )
82
+ )
83
+ ].join("\n")
84
+ );
85
+ const mountEntries = Object.entries(mounts);
86
+ sections.push(
87
+ mountEntries.length === 0 ? "Mounts: (empty)" : [
88
+ "Mounts:",
89
+ ...mountEntries.map(
90
+ ([name, mount]) => ` ${name} \u2192 ${mount.artifactDir} (${mount.built ? "built" : "not built"})`
91
+ )
92
+ ].join("\n")
93
+ );
94
+ return sections.join("\n\n");
95
+ }
96
+ export async function runValidate(rootDir, options = {}) {
97
+ const resolved = resolveDeclaration(rootDir);
98
+ if (!resolved) {
99
+ if (options.json) {
100
+ return {
101
+ exitCode: 0,
102
+ output: JSON.stringify(
103
+ { protocol: "legacy", diagnostics: [] },
104
+ null,
105
+ 4
106
+ )
107
+ };
108
+ }
109
+ return {
110
+ exitCode: 0,
111
+ output: [
112
+ 'No "esmx" field in package.json \u2014 this module uses the legacy entry.node.ts config.',
113
+ 'That is fine: the new module protocol is opt-in. To adopt it, add an "esmx" field to package.json (see the module protocol docs).'
114
+ ].join("\n")
115
+ };
116
+ }
117
+ const { diagnostics, supply } = resolved;
118
+ const mounts = resolved.mounts;
119
+ const hasError = diagnostics.some((d) => d.severity === "error");
120
+ const exitCode = hasError ? 1 : 0;
121
+ if (options.json) {
122
+ return {
123
+ exitCode,
124
+ output: JSON.stringify(
125
+ {
126
+ diagnostics: diagnostics.map(toEnvelopeEntry),
127
+ supply,
128
+ mounts
129
+ },
130
+ null,
131
+ 4
132
+ )
133
+ };
134
+ }
135
+ return {
136
+ exitCode,
137
+ output: formatHumanReport(diagnostics, supply, mounts)
138
+ };
139
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,216 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import url from "node:url";
4
+ import util from "node:util";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import {
7
+ createFixtureRoot,
8
+ removeFixtureRoot,
9
+ writeFixturePackage
10
+ } from "../declaration/test-fixtures.mjs";
11
+ import { runValidate } from "./validate.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
+ vi.restoreAllMocks();
20
+ await Promise.all(fixtureRoots.splice(0).map(removeFixtureRoot));
21
+ });
22
+ async function legacyFixture() {
23
+ const root = await fixtureRoot();
24
+ return writeFixturePackage(root, {
25
+ dir: "legacy",
26
+ packageJson: { name: "legacy", version: "1.0.0" }
27
+ });
28
+ }
29
+ async function warningOnlyFixture() {
30
+ const root = await fixtureRoot();
31
+ writeFixturePackage(root, {
32
+ dir: "app/node_modules/base",
33
+ packageJson: {
34
+ name: "base",
35
+ version: "1.0.0",
36
+ dependencies: { vue: "^3.4.0" },
37
+ esmx: { provides: ["vue"] }
38
+ },
39
+ built: true
40
+ });
41
+ writeFixturePackage(root, {
42
+ dir: "app/node_modules/base/node_modules/vue",
43
+ packageJson: { name: "vue", version: "3.4.21" }
44
+ });
45
+ return writeFixturePackage(root, {
46
+ dir: "app",
47
+ packageJson: {
48
+ name: "app",
49
+ version: "1.0.0",
50
+ esmx: {
51
+ entry: { client: "./src/entry.client.ts" },
52
+ uses: ["base"]
53
+ }
54
+ }
55
+ });
56
+ }
57
+ async function dupProviderFixture() {
58
+ const root = await fixtureRoot();
59
+ writeFixturePackage(root, {
60
+ dir: "node_modules/base",
61
+ packageJson: {
62
+ name: "base",
63
+ version: "1.0.0",
64
+ dependencies: { vue: "^3.4.0" },
65
+ esmx: { provides: ["vue"] }
66
+ },
67
+ built: true
68
+ });
69
+ writeFixturePackage(root, {
70
+ dir: "node_modules/base/node_modules/vue",
71
+ packageJson: { name: "vue", version: "3.4.21" }
72
+ });
73
+ writeFixturePackage(root, {
74
+ dir: "node_modules/vue-base",
75
+ packageJson: {
76
+ name: "vue-base",
77
+ version: "1.0.0",
78
+ dependencies: { vue: "^3.4.0" },
79
+ esmx: { uses: ["base"], provides: ["vue"] }
80
+ },
81
+ built: true
82
+ });
83
+ writeFixturePackage(root, {
84
+ dir: "node_modules/vue-base/node_modules/vue",
85
+ packageJson: { name: "vue", version: "3.5.13" }
86
+ });
87
+ return writeFixturePackage(root, {
88
+ dir: "app",
89
+ packageJson: {
90
+ name: "app",
91
+ version: "1.0.0",
92
+ dependencies: { vue: "^3.4.0" },
93
+ esmx: { uses: ["vue-base"] }
94
+ }
95
+ });
96
+ }
97
+ async function notLinkedFixture() {
98
+ const root = await fixtureRoot();
99
+ return writeFixturePackage(root, {
100
+ dir: "app",
101
+ packageJson: {
102
+ name: "app",
103
+ version: "1.0.0",
104
+ esmx: {
105
+ entry: { client: "./src/entry.client.ts" },
106
+ uses: ["ghost"]
107
+ }
108
+ }
109
+ });
110
+ }
111
+ describe("runValidate", () => {
112
+ it("should exit 0 with protocol legacy for a package without esmx field", async () => {
113
+ const appDir = await legacyFixture();
114
+ const jsonResult = await runValidate(appDir, { json: true });
115
+ const humanResult = await runValidate(appDir);
116
+ expect(jsonResult.exitCode).toBe(0);
117
+ expect(JSON.parse(jsonResult.output)).toEqual({
118
+ protocol: "legacy",
119
+ diagnostics: []
120
+ });
121
+ expect(humanResult.exitCode).toBe(0);
122
+ expect(humanResult.output).toContain("entry.node.ts");
123
+ expect(humanResult.output).toContain("opt-in");
124
+ });
125
+ it("should exit 0 when the declaration only has warnings", async () => {
126
+ const appDir = await warningOnlyFixture();
127
+ const result = await runValidate(appDir, { json: true });
128
+ const envelope = JSON.parse(result.output);
129
+ expect(result.exitCode).toBe(0);
130
+ expect(envelope.diagnostics).toHaveLength(1);
131
+ expect(envelope.diagnostics[0].code).toBe("W_NO_RANGE");
132
+ expect(envelope.supply.vue).toEqual({
133
+ groups: [{ major: 3, provider: "base", version: "3.4.21" }]
134
+ });
135
+ expect(Object.keys(envelope.mounts)).toEqual(["base"]);
136
+ expect(envelope.mounts.base.built).toBe(true);
137
+ });
138
+ it("should exit non-zero for E_NOT_LINKED with the exact envelope entry shape", async () => {
139
+ const appDir = await notLinkedFixture();
140
+ const result = await runValidate(appDir, { json: true });
141
+ const envelope = JSON.parse(result.output);
142
+ expect(result.exitCode).toBe(1);
143
+ const entry = envelope.diagnostics.find(
144
+ (d) => d.code === "E_NOT_LINKED"
145
+ );
146
+ expect(entry).toBeDefined();
147
+ expect(entry.module).toBe("app");
148
+ expect(typeof entry.message).toBe("string");
149
+ expect(typeof entry.fix).toBe("string");
150
+ expect(entry).not.toHaveProperty("severity");
151
+ });
152
+ it("should exit non-zero and surface E_DUP_PROVIDER in the JSON envelope", async () => {
153
+ const appDir = await dupProviderFixture();
154
+ const result = await runValidate(appDir, { json: true });
155
+ const envelope = JSON.parse(result.output);
156
+ expect(result.exitCode).toBe(1);
157
+ const entry = envelope.diagnostics.find(
158
+ (d) => d.code === "E_DUP_PROVIDER"
159
+ );
160
+ expect(entry).toBeDefined();
161
+ expect(entry.module).toBe("vue-base");
162
+ expect(entry.package).toBe("vue");
163
+ expect(entry.found).toBe("base, vue-base");
164
+ expect(envelope).not.toHaveProperty("elections");
165
+ });
166
+ it("should print E_DUP_PROVIDER in the human report", async () => {
167
+ const appDir = await dupProviderFixture();
168
+ const result = await runValidate(appDir);
169
+ const plain = util.stripVTControlCharacters(result.output);
170
+ expect(plain).toContain("[E_DUP_PROVIDER]");
171
+ expect(plain).toContain("vue-base \u2192 vue");
172
+ expect(plain).toContain("base, vue-base");
173
+ });
174
+ it("should report diagnostics, supply and mounts in human mode", async () => {
175
+ const appDir = await warningOnlyFixture();
176
+ const result = await runValidate(appDir);
177
+ const plain = util.stripVTControlCharacters(result.output);
178
+ expect(result.exitCode).toBe(0);
179
+ expect(plain).toContain("[W_NO_RANGE]");
180
+ expect(plain).toContain("fix:");
181
+ expect(plain).toContain("vue \u2192 base@3.4.21");
182
+ expect(plain).toMatch(/base โ†’ .*dist \(built\)/);
183
+ });
184
+ it("should produce pure JSON output without writing to the console", async () => {
185
+ const appDir = await notLinkedFixture();
186
+ const log = vi.spyOn(console, "log").mockImplementation(() => {
187
+ });
188
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {
189
+ });
190
+ const error = vi.spyOn(console, "error").mockImplementation(() => {
191
+ });
192
+ const result = await runValidate(appDir, { json: true });
193
+ expect(() => JSON.parse(result.output)).not.toThrow();
194
+ expect(log).not.toHaveBeenCalled();
195
+ expect(warn).not.toHaveBeenCalled();
196
+ expect(error).not.toHaveBeenCalled();
197
+ });
198
+ });
199
+ const distCli = path.resolve(
200
+ path.dirname(url.fileURLToPath(import.meta.url)),
201
+ "../../dist/cli/index.mjs"
202
+ );
203
+ describe.skipIf(!fs.existsSync(distCli))("esmx validate (spawned CLI)", () => {
204
+ it("should emit only the JSON envelope on stdout and exit non-zero on errors", async () => {
205
+ const childProcess = await import("node:child_process");
206
+ const appDir = await notLinkedFixture();
207
+ const spawned = childProcess.spawnSync(
208
+ process.execPath,
209
+ [distCli, "validate", "--json"],
210
+ { cwd: appDir, encoding: "utf-8" }
211
+ );
212
+ expect(spawned.status).toBe(1);
213
+ const envelope = JSON.parse(spawned.stdout);
214
+ expect(envelope.diagnostics[0].code).toBe("E_NOT_LINKED");
215
+ });
216
+ });
package/dist/core.d.ts CHANGED
@@ -298,6 +298,35 @@ export declare class Esmx {
298
298
  * ```
299
299
  */
300
300
  destroy(): Promise<boolean>;
301
+ /**
302
+ * Atomic generational relink (RFC 0001 ยง9).
303
+ *
304
+ * In-process remotes are not hot-swappable: the server import map and the
305
+ * module loader are captured once when the app is created. When a mounted
306
+ * remote republishes (new resolved versions, new wiring), the composer
307
+ * adopts it by relinking โ€” building a NEW generation (fresh cache,
308
+ * re-resolved module config, rebuilt app capturing a new import map +
309
+ * loader) and switching to it atomically.
310
+ *
311
+ * The previous generation keeps serving throughout the rebuild; the new
312
+ * app is installed only once it is fully built, then the previous app's
313
+ * resources are released. If the rebuild fails, the previous generation
314
+ * is fully restored (rollback) and the error propagates โ€” a failed relink
315
+ * never takes the running server down.
316
+ *
317
+ * Scope: the production render path (`start`/`preview`). Dev rebuilds its
318
+ * own app. Cross-compiler dev-watch invalidation remains future work.
319
+ *
320
+ * @returns true once the new generation is live.
321
+ * @throws {NotReadyError} when called before {@link init}.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * // After a mounted remote republishes:
326
+ * await esmx.reinit(); // new generation live, old requests drained
327
+ * ```
328
+ */
329
+ reinit(): Promise<boolean>;
301
330
  /**
302
331
  * Execute the application's build process.
303
332
  *
@@ -672,10 +701,12 @@ export declare class Esmx {
672
701
  src: string;
673
702
  filepath: string;
674
703
  code: string;
704
+ integrity: Record<string, string> | null;
675
705
  } : {
676
706
  src: null;
677
707
  filepath: null;
678
708
  code: string;
709
+ integrity: Record<string, string> | null;
679
710
  }>;
680
711
  /**
681
712
  * Get the list of static import paths for a module.