@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
package/README.md
CHANGED
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
|
|
30
30
|
## ๐ Features
|
|
31
31
|
|
|
32
|
-
- **Native Microfrontend** - Based on ESM +
|
|
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
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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
|
-
|
|
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}
|
|
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
|
-
`
|
|
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
|
|
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.
|