@esmx/import 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 +8 -1
- package/README.zh-CN.md +13 -6
- package/dist/error.test.mjs +38 -0
- package/dist/import-loader.mjs +5 -2
- package/dist/import-vm.d.ts +4 -1
- package/dist/import-vm.mjs +133 -55
- package/dist/import-vm.test.d.ts +1 -0
- package/dist/import-vm.test.mjs +653 -0
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +7 -0
- package/package.json +23 -6
- package/src/error.test.ts +46 -0
- package/src/import-loader.ts +5 -2
- package/src/import-vm.test.ts +755 -0
- package/src/import-vm.ts +222 -56
- package/src/index.ts +7 -2
- package/src/types.ts +8 -0
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
|
@@ -55,6 +55,13 @@ yarn add @esmx/import
|
|
|
55
55
|
import { createVmImport } from '@esmx/import';
|
|
56
56
|
import { pathToFileURL } from 'node:url';
|
|
57
57
|
|
|
58
|
+
const baseURL = pathToFileURL('/project');
|
|
59
|
+
const importMap = {
|
|
60
|
+
imports: {
|
|
61
|
+
'my-app/src/utils': '/project/src/utils.mjs'
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
58
65
|
const vmImport = createVmImport(baseURL, importMap);
|
|
59
66
|
const module = await vmImport('my-app/src/utils', import.meta.url);
|
|
60
67
|
```
|
|
@@ -124,7 +131,7 @@ const loaderImport = createLoaderImport(baseURL, importMap);
|
|
|
124
131
|
const module = await loaderImport(specifier);
|
|
125
132
|
```
|
|
126
133
|
|
|
127
|
-
###
|
|
134
|
+
### Import Map Format
|
|
128
135
|
```typescript
|
|
129
136
|
interface ImportMap {
|
|
130
137
|
imports?: Record<string, string>;
|
package/README.zh-CN.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</a>
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
|
-
<p>为 Esmx 框架提供 Import
|
|
23
|
+
<p>为 Esmx 框架提供 Import Map 的 Node.js 服务端实现</p>
|
|
24
24
|
|
|
25
25
|
<p>
|
|
26
26
|
<a href="https://github.com/esmnext/esmx/blob/master/packages/import/README.md">English</a> | 中文
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
|
|
32
32
|
- **双重实现** - 开发环境使用 VM 模式,生产环境使用 Loader 模式
|
|
33
33
|
- **热重载支持** - VM 模式支持多次创建,提供开发灵活性
|
|
34
|
-
- **高性能** - Loader
|
|
34
|
+
- **高性能** - Loader 模式针对生产环境优化
|
|
35
35
|
- **Node.js 专注** - 专为 Node.js 服务端环境设计
|
|
36
|
-
- **TypeScript
|
|
37
|
-
- **ESM 标准** - 完全符合 Import
|
|
36
|
+
- **TypeScript 支持** - 完整的 TypeScript 类型安全
|
|
37
|
+
- **ESM 标准** - 完全符合 Import Map 规范
|
|
38
38
|
|
|
39
39
|
## 📦 安装
|
|
40
40
|
|
|
@@ -55,13 +55,20 @@ yarn add @esmx/import
|
|
|
55
55
|
import { createVmImport } from '@esmx/import';
|
|
56
56
|
import { pathToFileURL } from 'node:url';
|
|
57
57
|
|
|
58
|
+
const baseURL = pathToFileURL('/project');
|
|
59
|
+
const importMap = {
|
|
60
|
+
imports: {
|
|
61
|
+
'my-app/src/utils': '/project/src/utils.mjs'
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
58
65
|
const vmImport = createVmImport(baseURL, importMap);
|
|
59
66
|
const module = await vmImport('my-app/src/utils', import.meta.url);
|
|
60
67
|
```
|
|
61
68
|
|
|
62
69
|
## 📖 模式对比
|
|
63
70
|
|
|
64
|
-
`@esmx/import` 提供两种不同的 Import
|
|
71
|
+
`@esmx/import` 提供两种不同的 Import Map 实现方式:
|
|
65
72
|
|
|
66
73
|
| 特性 | VM 模式 | Loader 模式 |
|
|
67
74
|
|------|---------|-------------|
|
|
@@ -124,7 +131,7 @@ const loaderImport = createLoaderImport(baseURL, importMap);
|
|
|
124
131
|
const module = await loaderImport(specifier);
|
|
125
132
|
```
|
|
126
133
|
|
|
127
|
-
###
|
|
134
|
+
### Import Map 格式
|
|
128
135
|
```typescript
|
|
129
136
|
interface ImportMap {
|
|
130
137
|
imports?: Record<string, string>;
|
package/dist/error.test.mjs
CHANGED
|
@@ -163,5 +163,43 @@ describe("Module Loading Errors", () => {
|
|
|
163
163
|
` \u2514\u2500 ${relativeE} \u274C Loading failed`
|
|
164
164
|
);
|
|
165
165
|
});
|
|
166
|
+
it("should handle formatModuleChain with original error", () => {
|
|
167
|
+
const moduleIds = [];
|
|
168
|
+
const targetModule = "/src/error.js";
|
|
169
|
+
const originalError = new Error("Original error message");
|
|
170
|
+
const formatted = formatModuleChain(
|
|
171
|
+
moduleIds,
|
|
172
|
+
targetModule,
|
|
173
|
+
originalError
|
|
174
|
+
);
|
|
175
|
+
expect(formatted).toContain("Error details:");
|
|
176
|
+
expect(formatted).toContain("Original error message");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe("ModuleLoadingError base class", () => {
|
|
180
|
+
it("should create base error with all properties", () => {
|
|
181
|
+
const moduleIds = ["/src/a.js"];
|
|
182
|
+
const targetModule = "/src/b.js";
|
|
183
|
+
const originalError = new Error("cause");
|
|
184
|
+
const error = new ModuleLoadingError(
|
|
185
|
+
"Base error",
|
|
186
|
+
moduleIds,
|
|
187
|
+
targetModule,
|
|
188
|
+
originalError
|
|
189
|
+
);
|
|
190
|
+
expect(error.name).toBe("ModuleLoadingError");
|
|
191
|
+
expect(error.message).toBe("Base error");
|
|
192
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
193
|
+
expect(error.targetModule).toBe(targetModule);
|
|
194
|
+
expect(error.originalError).toBe(originalError);
|
|
195
|
+
});
|
|
196
|
+
it("should create base error without original error", () => {
|
|
197
|
+
const error = new ModuleLoadingError(
|
|
198
|
+
"Simple error",
|
|
199
|
+
[],
|
|
200
|
+
"/src/x.js"
|
|
201
|
+
);
|
|
202
|
+
expect(error.originalError).toBeUndefined();
|
|
203
|
+
});
|
|
166
204
|
});
|
|
167
205
|
});
|
package/dist/import-loader.mjs
CHANGED
|
@@ -13,14 +13,17 @@ export function createLoaderImport(baseURL, importMap = {}) {
|
|
|
13
13
|
registered = JSON.stringify(importMap);
|
|
14
14
|
} else if (registered !== JSON.stringify(importMap)) {
|
|
15
15
|
throw new Error(
|
|
16
|
-
`
|
|
16
|
+
`[@esmx/import] createLoaderImport() is a per-process singleton and was called twice with different importMap data. Ensure your app initializes the loader exactly once at startup.`
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
return (specifier) => {
|
|
20
20
|
try {
|
|
21
21
|
return import(specifier);
|
|
22
22
|
} catch (e) {
|
|
23
|
-
|
|
23
|
+
const cause = e instanceof Error ? e.message : String(e);
|
|
24
|
+
throw new Error(
|
|
25
|
+
`[@esmx/import] Failed to import '${specifier}'. Verify the specifier is declared in the importMap (modules.imports in entry.node.ts) or matches a federated module name. Original error: ${cause}`
|
|
26
|
+
);
|
|
24
27
|
}
|
|
25
28
|
};
|
|
26
29
|
}
|
package/dist/import-vm.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import vm from 'node:vm';
|
|
2
2
|
import type { ImportMap } from './types';
|
|
3
|
-
export
|
|
3
|
+
export interface VmImportOptions {
|
|
4
|
+
readFileSync?: (path: string) => string;
|
|
5
|
+
}
|
|
6
|
+
export declare function createVmImport(baseURL: URL, importMap?: ImportMap, options?: VmImportOptions): (specifier: string, parent: string, sandbox?: vm.Context, options?: vm.CreateContextOptions) => Promise<Record<string, any>>;
|
package/dist/import-vm.mjs
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { isBuiltin } from "node:module";
|
|
2
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import vm from "node:vm";
|
|
6
|
-
import {
|
|
6
|
+
import { FileReadError } from "./error.mjs";
|
|
7
7
|
import { createImportMapResolver } from "./import-map-resolve.mjs";
|
|
8
|
+
const requireSync = createRequire(import.meta.url);
|
|
9
|
+
const STYLE_ASSET_RE = /\.(?:css|scss|sass|less|stylus|styl|pcss|postcss)(?:\?.*)?$/i;
|
|
10
|
+
function isStyleAsset(url) {
|
|
11
|
+
return STYLE_ASSET_RE.test(url);
|
|
12
|
+
}
|
|
13
|
+
let lazyGlobalsMaterialized = false;
|
|
14
|
+
function materializeLazyGlobals() {
|
|
15
|
+
if (lazyGlobalsMaterialized) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
lazyGlobalsMaterialized = true;
|
|
19
|
+
void globalThis.DOMException;
|
|
20
|
+
}
|
|
8
21
|
async function importBuiltinModule(specifier, context) {
|
|
9
22
|
const nodeModule = await import(specifier);
|
|
10
23
|
const keys = Object.keys(nodeModule);
|
|
@@ -26,7 +39,8 @@ async function importBuiltinModule(specifier, context) {
|
|
|
26
39
|
await module.evaluate();
|
|
27
40
|
return module;
|
|
28
41
|
}
|
|
29
|
-
export function createVmImport(baseURL, importMap = {}) {
|
|
42
|
+
export function createVmImport(baseURL, importMap = {}, options) {
|
|
43
|
+
const readFileSync = options?.readFileSync ?? ((path2) => fs.readFileSync(path2, "utf-8"));
|
|
30
44
|
const importMapResolver = createImportMapResolver(baseURL.href, importMap);
|
|
31
45
|
const buildMeta = (specifier, parent) => {
|
|
32
46
|
const result = importMapResolver(specifier, parent);
|
|
@@ -42,77 +56,141 @@ export function createVmImport(baseURL, importMap = {}) {
|
|
|
42
56
|
}
|
|
43
57
|
};
|
|
44
58
|
};
|
|
45
|
-
|
|
59
|
+
function syncLinker(specifier, parent, context, cache, linkStatus, moduleIds) {
|
|
46
60
|
if (isBuiltin(specifier)) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
const nodeModule = requireSync(specifier);
|
|
62
|
+
const keys = Object.keys(nodeModule);
|
|
63
|
+
const hasDefault = keys.includes("default");
|
|
64
|
+
if (!hasDefault) {
|
|
65
|
+
keys.push("default");
|
|
66
|
+
}
|
|
67
|
+
const module2 = new vm.SyntheticModule(
|
|
68
|
+
keys,
|
|
69
|
+
function evaluateCallback() {
|
|
70
|
+
keys.forEach((key) => {
|
|
71
|
+
this.setExport(
|
|
72
|
+
key,
|
|
73
|
+
key === "default" && !hasDefault ? nodeModule : nodeModule[key]
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
identifier: specifier,
|
|
79
|
+
context
|
|
80
|
+
}
|
|
55
81
|
);
|
|
82
|
+
const linkResult = module2.link(() => {
|
|
83
|
+
throw new TypeError(`Native modules should not be linked`);
|
|
84
|
+
});
|
|
85
|
+
const linkPromise2 = linkResult && typeof linkResult.then === "function" ? linkResult.then(() => module2.evaluate()) : module2.evaluate();
|
|
86
|
+
linkStatus.set(specifier, linkPromise2);
|
|
87
|
+
return module2;
|
|
56
88
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
const meta = buildMeta(specifier, parent);
|
|
90
|
+
const cachedModule = cache.get(meta.url);
|
|
91
|
+
if (cachedModule) {
|
|
92
|
+
return cachedModule;
|
|
60
93
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
async function moduleBuild() {
|
|
69
|
-
let text;
|
|
70
|
-
try {
|
|
71
|
-
text = fs.readFileSync(meta.filename, "utf-8");
|
|
72
|
-
} catch (error) {
|
|
73
|
-
throw new FileReadError(
|
|
74
|
-
`Failed to read module: ${meta.filename}`,
|
|
75
|
-
moduleIds,
|
|
76
|
-
meta.filename,
|
|
77
|
-
error
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
const module2 = new vm.SourceTextModule(text, {
|
|
81
|
-
initializeImportMeta: (importMeta) => {
|
|
82
|
-
Object.assign(importMeta, meta);
|
|
94
|
+
if (isStyleAsset(meta.url)) {
|
|
95
|
+
const exportNames = ["default", "href"];
|
|
96
|
+
const styleModule = new vm.SyntheticModule(
|
|
97
|
+
exportNames,
|
|
98
|
+
function evaluateCallback() {
|
|
99
|
+
this.setExport("default", meta.url);
|
|
100
|
+
this.setExport("href", meta.url);
|
|
83
101
|
},
|
|
84
|
-
identifier: specifier,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
cache,
|
|
92
|
-
[...moduleIds, meta.filename]
|
|
93
|
-
);
|
|
94
|
-
}
|
|
102
|
+
{ identifier: specifier, context }
|
|
103
|
+
);
|
|
104
|
+
cache.set(meta.url, styleModule);
|
|
105
|
+
const linkResult = styleModule.link(() => {
|
|
106
|
+
throw new TypeError(
|
|
107
|
+
`Style modules should not be linked: ${specifier}`
|
|
108
|
+
);
|
|
95
109
|
});
|
|
96
|
-
|
|
110
|
+
const linkPromise2 = linkResult && typeof linkResult.then === "function" ? linkResult.then(() => styleModule.evaluate()) : styleModule.evaluate();
|
|
111
|
+
linkStatus.set(meta.url, linkPromise2);
|
|
112
|
+
return styleModule;
|
|
113
|
+
}
|
|
114
|
+
let text;
|
|
115
|
+
try {
|
|
116
|
+
text = readFileSync(meta.filename);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new FileReadError(
|
|
119
|
+
`Failed to read module: ${meta.filename}`,
|
|
120
|
+
moduleIds,
|
|
121
|
+
meta.filename,
|
|
122
|
+
error
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const module = new vm.SourceTextModule(text, {
|
|
126
|
+
initializeImportMeta: (importMeta) => {
|
|
127
|
+
Object.assign(importMeta, meta);
|
|
128
|
+
},
|
|
129
|
+
identifier: specifier,
|
|
130
|
+
context,
|
|
131
|
+
importModuleDynamically: (specifier2, referrer) => {
|
|
97
132
|
return moduleLinker(
|
|
98
133
|
specifier2,
|
|
99
134
|
meta.url,
|
|
100
135
|
referrer.context,
|
|
101
136
|
cache,
|
|
137
|
+
linkStatus,
|
|
102
138
|
[...moduleIds, meta.filename]
|
|
103
139
|
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
cache.set(meta.url, module);
|
|
143
|
+
const linkPromise = module.link((specifier2, referrer) => {
|
|
144
|
+
return syncLinker(
|
|
145
|
+
specifier2,
|
|
146
|
+
meta.url,
|
|
147
|
+
referrer.context,
|
|
148
|
+
cache,
|
|
149
|
+
linkStatus,
|
|
150
|
+
[...moduleIds, meta.filename]
|
|
151
|
+
);
|
|
152
|
+
}).then(() => module.evaluate());
|
|
153
|
+
linkStatus.set(meta.url, linkPromise);
|
|
154
|
+
return module;
|
|
155
|
+
}
|
|
156
|
+
async function moduleLinker(specifier, parent, context, cache, linkStatus, moduleIds) {
|
|
157
|
+
if (isBuiltin(specifier)) {
|
|
158
|
+
return importBuiltinModule(specifier, context);
|
|
159
|
+
}
|
|
160
|
+
const meta = buildMeta(specifier, parent);
|
|
161
|
+
const cachedModule = cache.get(meta.url);
|
|
162
|
+
if (cachedModule) {
|
|
163
|
+
const status2 = linkStatus.get(meta.url);
|
|
164
|
+
if (status2) {
|
|
165
|
+
await status2;
|
|
166
|
+
}
|
|
167
|
+
return cachedModule;
|
|
168
|
+
}
|
|
169
|
+
const module = syncLinker(
|
|
170
|
+
specifier,
|
|
171
|
+
parent,
|
|
172
|
+
context,
|
|
173
|
+
cache,
|
|
174
|
+
linkStatus,
|
|
175
|
+
moduleIds
|
|
176
|
+
);
|
|
177
|
+
const status = linkStatus.get(meta.url);
|
|
178
|
+
if (status) {
|
|
179
|
+
await status;
|
|
107
180
|
}
|
|
181
|
+
return module;
|
|
108
182
|
}
|
|
109
|
-
return async (specifier, parent, sandbox,
|
|
110
|
-
|
|
183
|
+
return async (specifier, parent, sandbox, options2) => {
|
|
184
|
+
materializeLazyGlobals();
|
|
185
|
+
const context = vm.createContext(sandbox, options2);
|
|
186
|
+
const cache = /* @__PURE__ */ new Map();
|
|
187
|
+
const linkStatus = /* @__PURE__ */ new Map();
|
|
111
188
|
const module = await moduleLinker(
|
|
112
189
|
specifier,
|
|
113
190
|
parent,
|
|
114
191
|
context,
|
|
115
|
-
|
|
192
|
+
cache,
|
|
193
|
+
linkStatus,
|
|
116
194
|
[]
|
|
117
195
|
);
|
|
118
196
|
return module.namespace;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|