@esmx/core 3.0.0-rc.116 → 3.0.0-rc.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -3
- package/README.zh-CN.md +20 -6
- package/dist/app.mjs +4 -2
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.mjs +28 -1
- package/dist/cli/validate.d.ts +15 -0
- package/dist/cli/validate.mjs +139 -0
- package/dist/cli/validate.test.d.ts +1 -0
- package/dist/cli/validate.test.mjs +216 -0
- package/dist/core.d.ts +31 -0
- package/dist/core.mjs +71 -5
- package/dist/declaration/index.d.ts +47 -0
- package/dist/declaration/index.mjs +63 -0
- package/dist/declaration/index.test.d.ts +1 -0
- package/dist/declaration/index.test.mjs +202 -0
- package/dist/declaration/lower.d.ts +11 -0
- package/dist/declaration/lower.mjs +78 -0
- package/dist/declaration/lower.test.d.ts +1 -0
- package/dist/declaration/lower.test.mjs +333 -0
- package/dist/declaration/reader.d.ts +28 -0
- package/dist/declaration/reader.mjs +55 -0
- package/dist/declaration/reinit.e2e.test.d.ts +1 -0
- package/dist/declaration/reinit.e2e.test.mjs +117 -0
- package/dist/declaration/resolver.d.ts +59 -0
- package/dist/declaration/resolver.mjs +430 -0
- package/dist/declaration/resolver.test.d.ts +1 -0
- package/dist/declaration/resolver.test.mjs +1005 -0
- package/dist/declaration/schema.d.ts +89 -0
- package/dist/declaration/schema.mjs +282 -0
- package/dist/declaration/schema.test.d.ts +1 -0
- package/dist/declaration/schema.test.mjs +101 -0
- package/dist/declaration/semver.d.ts +22 -0
- package/dist/declaration/semver.mjs +229 -0
- package/dist/declaration/semver.test.d.ts +1 -0
- package/dist/declaration/semver.test.mjs +87 -0
- package/dist/declaration/test-fixtures.d.ts +18 -0
- package/dist/declaration/test-fixtures.mjs +69 -0
- package/dist/declaration/types.d.ts +58 -0
- package/dist/declaration/types.mjs +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +10 -0
- package/dist/manifest-json.d.ts +61 -0
- package/dist/manifest-json.mjs +68 -5
- package/dist/manifest-json.test.d.ts +1 -0
- package/dist/manifest-json.test.mjs +196 -0
- package/dist/module-config.d.ts +45 -5
- package/dist/module-config.mjs +60 -49
- package/dist/module-config.test.mjs +227 -91
- package/dist/pack-config.d.ts +2 -9
- package/dist/render-context.mjs +22 -5
- package/dist/utils/import-map.d.ts +23 -3
- package/dist/utils/import-map.mjs +38 -1
- package/dist/utils/import-map.test.mjs +228 -0
- package/package.json +9 -6
- package/src/app.ts +5 -2
- package/src/cli/cli.ts +44 -1
- package/src/cli/validate.test.ts +251 -0
- package/src/cli/validate.ts +196 -0
- package/src/core.ts +84 -5
- package/src/declaration/index.test.ts +223 -0
- package/src/declaration/index.ts +135 -0
- package/src/declaration/lower.test.ts +372 -0
- package/src/declaration/lower.ts +135 -0
- package/src/declaration/reader.ts +93 -0
- package/src/declaration/reinit.e2e.test.ts +148 -0
- package/src/declaration/resolver.test.ts +1111 -0
- package/src/declaration/resolver.ts +638 -0
- package/src/declaration/schema.test.ts +118 -0
- package/src/declaration/schema.ts +339 -0
- package/src/declaration/semver.test.ts +101 -0
- package/src/declaration/semver.ts +278 -0
- package/src/declaration/test-fixtures.ts +96 -0
- package/src/declaration/types.ts +71 -0
- package/src/index.ts +28 -1
- package/src/manifest-json.test.ts +236 -0
- package/src/manifest-json.ts +166 -5
- package/src/module-config.test.ts +266 -105
- package/src/module-config.ts +130 -58
- package/src/pack-config.ts +2 -9
- package/src/render-context.ts +34 -6
- package/src/utils/import-map.test.ts +261 -0
- package/src/utils/import-map.ts +92 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Esmx Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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.
|