@esmx/import 3.0.0-rc.43 → 3.0.0-rc.45
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/dist/import-loader.d.ts +3 -1
- package/dist/import-loader.mjs +5 -12
- package/dist/import-map-resolve.d.ts +2 -0
- package/dist/import-map-resolve.mjs +13 -0
- package/dist/import-map-resolve.test.d.ts +1 -0
- package/dist/import-map-resolve.test.mjs +182 -0
- package/dist/import-vm.mjs +25 -34
- package/dist/types.d.ts +1 -0
- package/package.json +3 -3
- package/src/import-loader.ts +7 -13
- package/src/import-map-resolve.test.ts +224 -0
- package/src/import-map-resolve.ts +18 -0
- package/src/import-vm.ts +25 -37
- package/src/types.ts +5 -0
package/dist/import-loader.d.ts
CHANGED
|
@@ -5,5 +5,7 @@ interface Data {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function createLoaderImport(baseURL: URL, importMap?: ImportMap): (specifier: string) => Promise<Record<string, any>>;
|
|
7
7
|
export declare function initialize(data: Data): void;
|
|
8
|
-
export declare function resolve(specifier: string, context:
|
|
8
|
+
export declare function resolve(specifier: string, context: {
|
|
9
|
+
parentURL: string;
|
|
10
|
+
}, nextResolve: Function): any;
|
|
9
11
|
export {};
|
package/dist/import-loader.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import module from "node:module";
|
|
2
|
-
import {
|
|
3
|
-
import IM from "@import-maps/resolve";
|
|
2
|
+
import { createImportMapResolver } from "./import-map-resolve.mjs";
|
|
4
3
|
let registered = "";
|
|
5
4
|
export function createLoaderImport(baseURL, importMap = {}) {
|
|
6
5
|
if (!registered) {
|
|
@@ -25,17 +24,11 @@ export function createLoaderImport(baseURL, importMap = {}) {
|
|
|
25
24
|
}
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
|
-
let
|
|
29
|
-
let loaderParsedImportMap = {};
|
|
27
|
+
let importMapResolver = null;
|
|
30
28
|
export function initialize(data) {
|
|
31
|
-
|
|
32
|
-
loaderParsedImportMap = IM.parse(data.importMap, loaderBaseURL);
|
|
29
|
+
importMapResolver = createImportMapResolver(data.baseURL, data.importMap);
|
|
33
30
|
}
|
|
34
31
|
export function resolve(specifier, context, nextResolve) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
if (result.matched && result.resolvedImport) {
|
|
38
|
-
return nextResolve(pathToFileURL(result.resolvedImport.pathname).href);
|
|
39
|
-
}
|
|
40
|
-
return nextResolve(specifier, context);
|
|
32
|
+
const result = importMapResolver?.(specifier, context.parentURL);
|
|
33
|
+
return nextResolve(result ?? specifier, context);
|
|
41
34
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { parse, resolve } from "@import-maps/resolve";
|
|
3
|
+
export function createImportMapResolver(base, importMap) {
|
|
4
|
+
const baseURL = pathToFileURL(base);
|
|
5
|
+
const parsedImportMap = parse(importMap, baseURL);
|
|
6
|
+
return (specifier, scriptURL) => {
|
|
7
|
+
const result = resolve(specifier, parsedImportMap, new URL(scriptURL));
|
|
8
|
+
if (result.resolvedImport) {
|
|
9
|
+
return result.resolvedImport.href;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createImportMapResolver } from "./import-map-resolve.mjs";
|
|
3
|
+
describe("createImportMapResolver", () => {
|
|
4
|
+
describe("basic import resolution", () => {
|
|
5
|
+
it("resolves imports correctly on Windows", () => {
|
|
6
|
+
const base = "file:///C:/projects/app";
|
|
7
|
+
const importMap = {
|
|
8
|
+
imports: {
|
|
9
|
+
lodash: "https://cdn.skypack.dev/lodash",
|
|
10
|
+
"utils/": "file:///C:/projects/app/src/utils/",
|
|
11
|
+
"./components/": "./components/"
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
15
|
+
expect(resolver("lodash", "file:///C:/projects/app/index.js")).toBe(
|
|
16
|
+
"https://cdn.skypack.dev/lodash"
|
|
17
|
+
);
|
|
18
|
+
expect(
|
|
19
|
+
resolver("utils/math.js", "file:///C:/projects/app/index.js")
|
|
20
|
+
).toBe("file:///C:/projects/app/src/utils/math.js");
|
|
21
|
+
expect(
|
|
22
|
+
resolver(
|
|
23
|
+
"./components/button.js",
|
|
24
|
+
"file:///C:/projects/app/index.js"
|
|
25
|
+
)
|
|
26
|
+
).toBe("file:///C:/projects/app/components/button.js");
|
|
27
|
+
});
|
|
28
|
+
it("resolves imports correctly on Unix", () => {
|
|
29
|
+
const base = "file:///opt/projects/app";
|
|
30
|
+
const importMap = {
|
|
31
|
+
imports: {
|
|
32
|
+
lodash: "https://cdn.skypack.dev/lodash",
|
|
33
|
+
"utils/": "file:///opt/projects/app/src/utils/",
|
|
34
|
+
"./components/": "./components/"
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
38
|
+
expect(
|
|
39
|
+
resolver("lodash", "file:///opt/projects/app/index.js")
|
|
40
|
+
).toBe("https://cdn.skypack.dev/lodash");
|
|
41
|
+
expect(
|
|
42
|
+
resolver("utils/math.js", "file:///opt/projects/app/index.js")
|
|
43
|
+
).toBe("file:///opt/projects/app/src/utils/math.js");
|
|
44
|
+
expect(
|
|
45
|
+
resolver(
|
|
46
|
+
"./components/button.js",
|
|
47
|
+
"file:///opt/projects/app/index.js"
|
|
48
|
+
)
|
|
49
|
+
).toBe("file:///opt/projects/app/components/button.js");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("scopes resolution", () => {
|
|
53
|
+
it("resolves imports from different scopes", () => {
|
|
54
|
+
const base = "file:///opt/projects/app";
|
|
55
|
+
const importMap = {
|
|
56
|
+
imports: {
|
|
57
|
+
"components/": "file:///opt/projects/app/src/components/",
|
|
58
|
+
lodash: "https://cdn.skypack.dev/lodash@4.17.21"
|
|
59
|
+
},
|
|
60
|
+
scopes: {
|
|
61
|
+
"file:///opt/projects/app/src/admin/": {
|
|
62
|
+
"components/": "file:///opt/projects/app/src/admin/components/",
|
|
63
|
+
lodash: "https://cdn.skypack.dev/lodash@4.17.15"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
68
|
+
expect(
|
|
69
|
+
resolver(
|
|
70
|
+
"components/button.js",
|
|
71
|
+
"file:///opt/projects/app/index.js"
|
|
72
|
+
)
|
|
73
|
+
).toBe("file:///opt/projects/app/src/components/button.js");
|
|
74
|
+
expect(
|
|
75
|
+
resolver("lodash", "file:///opt/projects/app/index.js")
|
|
76
|
+
).toBe("https://cdn.skypack.dev/lodash@4.17.21");
|
|
77
|
+
expect(
|
|
78
|
+
resolver(
|
|
79
|
+
"components/button.js",
|
|
80
|
+
"file:///opt/projects/app/src/admin/index.js"
|
|
81
|
+
)
|
|
82
|
+
).toBe("file:///opt/projects/app/src/admin/components/button.js");
|
|
83
|
+
expect(
|
|
84
|
+
resolver(
|
|
85
|
+
"lodash",
|
|
86
|
+
"file:///opt/projects/app/src/admin/index.js"
|
|
87
|
+
)
|
|
88
|
+
).toBe("https://cdn.skypack.dev/lodash@4.17.15");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("real world scenarios", () => {
|
|
92
|
+
it("resolves server-side rendering project imports", () => {
|
|
93
|
+
const base = "file:///opt/projects/example/server-app";
|
|
94
|
+
const importMap = {
|
|
95
|
+
imports: {
|
|
96
|
+
"app/server/entry": "file:///opt/projects/example/server-app/dist/server/exports/src/entry.server.mjs"
|
|
97
|
+
},
|
|
98
|
+
scopes: {
|
|
99
|
+
"file:///opt/projects/example/server-app/dist/server/": {}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
103
|
+
expect(
|
|
104
|
+
resolver(
|
|
105
|
+
"app/server/entry",
|
|
106
|
+
"file:///opt/projects/example/server-app/index.js"
|
|
107
|
+
)
|
|
108
|
+
).toBe(
|
|
109
|
+
"file:///opt/projects/example/server-app/dist/server/exports/src/entry.server.mjs"
|
|
110
|
+
);
|
|
111
|
+
expect(
|
|
112
|
+
resolver(
|
|
113
|
+
"app/server/entry",
|
|
114
|
+
"file:///opt/projects/example/server-app/dist/server/main.js"
|
|
115
|
+
)
|
|
116
|
+
).toBe(
|
|
117
|
+
"file:///opt/projects/example/server-app/dist/server/exports/src/entry.server.mjs"
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("unresolved specifiers", () => {
|
|
122
|
+
it("returns null for unresolved specifiers", () => {
|
|
123
|
+
const base = "file:///opt/projects/app";
|
|
124
|
+
const importMap = {
|
|
125
|
+
imports: {
|
|
126
|
+
"utils/": "file:///opt/projects/app/src/utils/"
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
130
|
+
expect(
|
|
131
|
+
resolver(
|
|
132
|
+
"components/button.js",
|
|
133
|
+
"file:///opt/projects/app/index.js"
|
|
134
|
+
)
|
|
135
|
+
).toBe(null);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("edge cases", () => {
|
|
139
|
+
it("handles empty import map", () => {
|
|
140
|
+
const base = "file:///opt/projects/app";
|
|
141
|
+
const importMap = {};
|
|
142
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
143
|
+
expect(
|
|
144
|
+
resolver("lodash", "file:///opt/projects/app/index.js")
|
|
145
|
+
).toBe(null);
|
|
146
|
+
});
|
|
147
|
+
it("handles spaces in paths", () => {
|
|
148
|
+
const base = "file:///opt/projects/My%20Project/app";
|
|
149
|
+
const importMap = {
|
|
150
|
+
imports: {
|
|
151
|
+
"components/": "file:///opt/projects/My%20Project/app/src/my%20components/"
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
155
|
+
expect(
|
|
156
|
+
resolver(
|
|
157
|
+
"components/button.js",
|
|
158
|
+
"file:///opt/projects/My%20Project/app/index.js"
|
|
159
|
+
)
|
|
160
|
+
).toBe(
|
|
161
|
+
"file:///opt/projects/My%20Project/app/src/my%20components/button.js"
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
it("handles Unicode characters in paths", () => {
|
|
165
|
+
const base = "file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app";
|
|
166
|
+
const importMap = {
|
|
167
|
+
imports: {
|
|
168
|
+
"\u56FD\u9645\u5316/": "file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app/src/%E5%9B%BD%E9%99%85%E5%8C%96/"
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
172
|
+
expect(
|
|
173
|
+
resolver(
|
|
174
|
+
"\u56FD\u9645\u5316/zh.js",
|
|
175
|
+
"file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app/index.js"
|
|
176
|
+
)
|
|
177
|
+
).toBe(
|
|
178
|
+
"file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app/src/%E5%9B%BD%E9%99%85%E5%8C%96/zh.js"
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
package/dist/import-vm.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { isBuiltin } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import vm from "node:vm";
|
|
6
|
-
import IM from "@import-maps/resolve";
|
|
7
6
|
import { CircularDependencyError, FileReadError } from "./error.mjs";
|
|
7
|
+
import { createImportMapResolver } from "./import-map-resolve.mjs";
|
|
8
8
|
async function importBuiltinModule(specifier, context) {
|
|
9
9
|
const nodeModule = await import(specifier);
|
|
10
10
|
const keys = Object.keys(nodeModule);
|
|
@@ -27,36 +27,33 @@ async function importBuiltinModule(specifier, context) {
|
|
|
27
27
|
return module;
|
|
28
28
|
}
|
|
29
29
|
export function createVmImport(baseURL, importMap = {}) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const result =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
filename = result.resolvedImport.href;
|
|
36
|
-
} else {
|
|
37
|
-
filename = import.meta.resolve(specifier, parent);
|
|
38
|
-
}
|
|
39
|
-
const url = pathToFileURL(filename);
|
|
40
|
-
const pathname = fileURLToPath(url);
|
|
30
|
+
const importMapResolver = createImportMapResolver(baseURL.href, importMap);
|
|
31
|
+
const buildMeta = (specifier, parent) => {
|
|
32
|
+
const result = importMapResolver(specifier, parent);
|
|
33
|
+
const url = result ?? import.meta.resolve(specifier, parent);
|
|
34
|
+
const filename = fileURLToPath(url);
|
|
41
35
|
return {
|
|
42
36
|
filename,
|
|
37
|
+
dirname: path.dirname(filename),
|
|
43
38
|
url,
|
|
44
|
-
|
|
39
|
+
resolve: (specifier2, parent2 = url) => {
|
|
40
|
+
return import.meta.resolve(specifier2, parent2);
|
|
41
|
+
}
|
|
45
42
|
};
|
|
46
43
|
};
|
|
47
44
|
async function moduleLinker(specifier, parent, context, cache, moduleIds) {
|
|
48
45
|
if (isBuiltin(specifier)) {
|
|
49
46
|
return importBuiltinModule(specifier, context);
|
|
50
47
|
}
|
|
51
|
-
const
|
|
52
|
-
if (moduleIds.includes(
|
|
48
|
+
const meta = buildMeta(specifier, parent);
|
|
49
|
+
if (moduleIds.includes(meta.filename)) {
|
|
53
50
|
throw new CircularDependencyError(
|
|
54
51
|
"Circular dependency detected",
|
|
55
52
|
moduleIds,
|
|
56
|
-
|
|
53
|
+
meta.filename
|
|
57
54
|
);
|
|
58
55
|
}
|
|
59
|
-
const module = cache.get(
|
|
56
|
+
const module = cache.get(meta.filename);
|
|
60
57
|
if (module) {
|
|
61
58
|
return module;
|
|
62
59
|
}
|
|
@@ -65,49 +62,43 @@ export function createVmImport(baseURL, importMap = {}) {
|
|
|
65
62
|
moduleBuild().then(resolve);
|
|
66
63
|
});
|
|
67
64
|
});
|
|
68
|
-
|
|
69
|
-
cache.set(parsed.pathname, modulePromise);
|
|
65
|
+
cache.set(meta.filename, modulePromise);
|
|
70
66
|
return modulePromise;
|
|
71
67
|
async function moduleBuild() {
|
|
72
68
|
let text;
|
|
73
69
|
try {
|
|
74
|
-
text = fs.readFileSync(
|
|
70
|
+
text = fs.readFileSync(meta.filename, "utf-8");
|
|
75
71
|
} catch (error) {
|
|
76
72
|
throw new FileReadError(
|
|
77
|
-
`Failed to read module: ${
|
|
73
|
+
`Failed to read module: ${meta.filename}`,
|
|
78
74
|
moduleIds,
|
|
79
|
-
|
|
75
|
+
meta.filename,
|
|
80
76
|
error
|
|
81
77
|
);
|
|
82
78
|
}
|
|
83
79
|
const module2 = new vm.SourceTextModule(text, {
|
|
84
|
-
initializeImportMeta: (
|
|
85
|
-
|
|
86
|
-
meta.dirname = dirname;
|
|
87
|
-
meta.resolve = (specifier2, parent2 = parsed.url) => {
|
|
88
|
-
return import.meta.resolve(specifier2, parent2);
|
|
89
|
-
};
|
|
90
|
-
meta.url = parsed.url.toString();
|
|
80
|
+
initializeImportMeta: (importMeta) => {
|
|
81
|
+
Object.assign(importMeta, meta);
|
|
91
82
|
},
|
|
92
83
|
identifier: specifier,
|
|
93
84
|
context,
|
|
94
85
|
importModuleDynamically: (specifier2, referrer) => {
|
|
95
86
|
return moduleLinker(
|
|
96
87
|
specifier2,
|
|
97
|
-
|
|
88
|
+
meta.filename,
|
|
98
89
|
referrer.context,
|
|
99
90
|
cache,
|
|
100
|
-
[...moduleIds,
|
|
91
|
+
[...moduleIds, meta.filename]
|
|
101
92
|
);
|
|
102
93
|
}
|
|
103
94
|
});
|
|
104
95
|
await module2.link((specifier2, referrer) => {
|
|
105
96
|
return moduleLinker(
|
|
106
97
|
specifier2,
|
|
107
|
-
|
|
98
|
+
meta.filename,
|
|
108
99
|
referrer.context,
|
|
109
100
|
cache,
|
|
110
|
-
[...moduleIds,
|
|
101
|
+
[...moduleIds, meta.filename]
|
|
111
102
|
);
|
|
112
103
|
});
|
|
113
104
|
await module2.evaluate();
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@biomejs/biome": "1.9.4",
|
|
36
|
-
"@esmx/lint": "3.0.0-rc.
|
|
36
|
+
"@esmx/lint": "3.0.0-rc.45",
|
|
37
37
|
"@types/node": "^24.0.0",
|
|
38
38
|
"@vitest/coverage-v8": "3.2.4",
|
|
39
39
|
"stylelint": "16.21.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"unbuild": "3.5.0",
|
|
42
42
|
"vitest": "3.2.4"
|
|
43
43
|
},
|
|
44
|
-
"version": "3.0.0-rc.
|
|
44
|
+
"version": "3.0.0-rc.45",
|
|
45
45
|
"type": "module",
|
|
46
46
|
"private": false,
|
|
47
47
|
"exports": {
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"template",
|
|
61
61
|
"public"
|
|
62
62
|
],
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "c1e2c9bf24c9321b06e63a08e8bfc4953c834aef"
|
|
64
64
|
}
|
package/src/import-loader.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import module from 'node:module';
|
|
2
|
-
import { pathToFileURL } from 'node:url';
|
|
3
2
|
import IM from '@import-maps/resolve';
|
|
4
|
-
import
|
|
3
|
+
import { createImportMapResolver } from './import-map-resolve';
|
|
4
|
+
import type { ImportMap, ImportMapResolver } from './types';
|
|
5
5
|
|
|
6
6
|
interface Data {
|
|
7
7
|
baseURL: string;
|
|
@@ -34,23 +34,17 @@ export function createLoaderImport(baseURL: URL, importMap: ImportMap = {}) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
let
|
|
38
|
-
let loaderParsedImportMap: IM.ParsedImportMap = {};
|
|
37
|
+
let importMapResolver: ImportMapResolver | null = null;
|
|
39
38
|
|
|
40
39
|
export function initialize(data: Data) {
|
|
41
|
-
|
|
42
|
-
loaderParsedImportMap = IM.parse(data.importMap, loaderBaseURL);
|
|
40
|
+
importMapResolver = createImportMapResolver(data.baseURL, data.importMap);
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
export function resolve(
|
|
46
44
|
specifier: string,
|
|
47
|
-
context:
|
|
45
|
+
context: { parentURL: string },
|
|
48
46
|
nextResolve: Function
|
|
49
47
|
) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (result.matched && result.resolvedImport) {
|
|
53
|
-
return nextResolve(pathToFileURL(result.resolvedImport.pathname).href);
|
|
54
|
-
}
|
|
55
|
-
return nextResolve(specifier, context);
|
|
48
|
+
const result = importMapResolver?.(specifier, context.parentURL);
|
|
49
|
+
return nextResolve(result ?? specifier, context);
|
|
56
50
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createImportMapResolver } from './import-map-resolve';
|
|
3
|
+
import type { ImportMap } from './types';
|
|
4
|
+
|
|
5
|
+
describe('createImportMapResolver', () => {
|
|
6
|
+
describe('basic import resolution', () => {
|
|
7
|
+
it('resolves imports correctly on Windows', () => {
|
|
8
|
+
const base = 'file:///C:/projects/app';
|
|
9
|
+
|
|
10
|
+
const importMap: ImportMap = {
|
|
11
|
+
imports: {
|
|
12
|
+
lodash: 'https://cdn.skypack.dev/lodash',
|
|
13
|
+
'utils/': 'file:///C:/projects/app/src/utils/',
|
|
14
|
+
'./components/': './components/'
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
19
|
+
|
|
20
|
+
expect(resolver('lodash', 'file:///C:/projects/app/index.js')).toBe(
|
|
21
|
+
'https://cdn.skypack.dev/lodash'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
resolver('utils/math.js', 'file:///C:/projects/app/index.js')
|
|
26
|
+
).toBe('file:///C:/projects/app/src/utils/math.js');
|
|
27
|
+
|
|
28
|
+
expect(
|
|
29
|
+
resolver(
|
|
30
|
+
'./components/button.js',
|
|
31
|
+
'file:///C:/projects/app/index.js'
|
|
32
|
+
)
|
|
33
|
+
).toBe('file:///C:/projects/app/components/button.js');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('resolves imports correctly on Unix', () => {
|
|
37
|
+
const base = 'file:///opt/projects/app';
|
|
38
|
+
|
|
39
|
+
const importMap: ImportMap = {
|
|
40
|
+
imports: {
|
|
41
|
+
lodash: 'https://cdn.skypack.dev/lodash',
|
|
42
|
+
'utils/': 'file:///opt/projects/app/src/utils/',
|
|
43
|
+
'./components/': './components/'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
48
|
+
|
|
49
|
+
expect(
|
|
50
|
+
resolver('lodash', 'file:///opt/projects/app/index.js')
|
|
51
|
+
).toBe('https://cdn.skypack.dev/lodash');
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
resolver('utils/math.js', 'file:///opt/projects/app/index.js')
|
|
55
|
+
).toBe('file:///opt/projects/app/src/utils/math.js');
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
resolver(
|
|
59
|
+
'./components/button.js',
|
|
60
|
+
'file:///opt/projects/app/index.js'
|
|
61
|
+
)
|
|
62
|
+
).toBe('file:///opt/projects/app/components/button.js');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('scopes resolution', () => {
|
|
67
|
+
it('resolves imports from different scopes', () => {
|
|
68
|
+
const base = 'file:///opt/projects/app';
|
|
69
|
+
|
|
70
|
+
const importMap: ImportMap = {
|
|
71
|
+
imports: {
|
|
72
|
+
'components/': 'file:///opt/projects/app/src/components/',
|
|
73
|
+
lodash: 'https://cdn.skypack.dev/lodash@4.17.21'
|
|
74
|
+
},
|
|
75
|
+
scopes: {
|
|
76
|
+
'file:///opt/projects/app/src/admin/': {
|
|
77
|
+
'components/':
|
|
78
|
+
'file:///opt/projects/app/src/admin/components/',
|
|
79
|
+
lodash: 'https://cdn.skypack.dev/lodash@4.17.15'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
resolver(
|
|
88
|
+
'components/button.js',
|
|
89
|
+
'file:///opt/projects/app/index.js'
|
|
90
|
+
)
|
|
91
|
+
).toBe('file:///opt/projects/app/src/components/button.js');
|
|
92
|
+
expect(
|
|
93
|
+
resolver('lodash', 'file:///opt/projects/app/index.js')
|
|
94
|
+
).toBe('https://cdn.skypack.dev/lodash@4.17.21');
|
|
95
|
+
|
|
96
|
+
expect(
|
|
97
|
+
resolver(
|
|
98
|
+
'components/button.js',
|
|
99
|
+
'file:///opt/projects/app/src/admin/index.js'
|
|
100
|
+
)
|
|
101
|
+
).toBe('file:///opt/projects/app/src/admin/components/button.js');
|
|
102
|
+
expect(
|
|
103
|
+
resolver(
|
|
104
|
+
'lodash',
|
|
105
|
+
'file:///opt/projects/app/src/admin/index.js'
|
|
106
|
+
)
|
|
107
|
+
).toBe('https://cdn.skypack.dev/lodash@4.17.15');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('real world scenarios', () => {
|
|
112
|
+
it('resolves server-side rendering project imports', () => {
|
|
113
|
+
const base = 'file:///opt/projects/example/server-app';
|
|
114
|
+
|
|
115
|
+
const importMap: ImportMap = {
|
|
116
|
+
imports: {
|
|
117
|
+
'app/server/entry':
|
|
118
|
+
'file:///opt/projects/example/server-app/dist/server/exports/src/entry.server.mjs'
|
|
119
|
+
},
|
|
120
|
+
scopes: {
|
|
121
|
+
'file:///opt/projects/example/server-app/dist/server/': {}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
126
|
+
|
|
127
|
+
expect(
|
|
128
|
+
resolver(
|
|
129
|
+
'app/server/entry',
|
|
130
|
+
'file:///opt/projects/example/server-app/index.js'
|
|
131
|
+
)
|
|
132
|
+
).toBe(
|
|
133
|
+
'file:///opt/projects/example/server-app/dist/server/exports/src/entry.server.mjs'
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(
|
|
137
|
+
resolver(
|
|
138
|
+
'app/server/entry',
|
|
139
|
+
'file:///opt/projects/example/server-app/dist/server/main.js'
|
|
140
|
+
)
|
|
141
|
+
).toBe(
|
|
142
|
+
'file:///opt/projects/example/server-app/dist/server/exports/src/entry.server.mjs'
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('unresolved specifiers', () => {
|
|
148
|
+
it('returns null for unresolved specifiers', () => {
|
|
149
|
+
const base = 'file:///opt/projects/app';
|
|
150
|
+
|
|
151
|
+
const importMap: ImportMap = {
|
|
152
|
+
imports: {
|
|
153
|
+
'utils/': 'file:///opt/projects/app/src/utils/'
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
158
|
+
|
|
159
|
+
expect(
|
|
160
|
+
resolver(
|
|
161
|
+
'components/button.js',
|
|
162
|
+
'file:///opt/projects/app/index.js'
|
|
163
|
+
)
|
|
164
|
+
).toBe(null);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('edge cases', () => {
|
|
169
|
+
it('handles empty import map', () => {
|
|
170
|
+
const base = 'file:///opt/projects/app';
|
|
171
|
+
const importMap: ImportMap = {};
|
|
172
|
+
|
|
173
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
174
|
+
|
|
175
|
+
expect(
|
|
176
|
+
resolver('lodash', 'file:///opt/projects/app/index.js')
|
|
177
|
+
).toBe(null);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('handles spaces in paths', () => {
|
|
181
|
+
const base = 'file:///opt/projects/My%20Project/app';
|
|
182
|
+
|
|
183
|
+
const importMap: ImportMap = {
|
|
184
|
+
imports: {
|
|
185
|
+
'components/':
|
|
186
|
+
'file:///opt/projects/My%20Project/app/src/my%20components/'
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
191
|
+
|
|
192
|
+
expect(
|
|
193
|
+
resolver(
|
|
194
|
+
'components/button.js',
|
|
195
|
+
'file:///opt/projects/My%20Project/app/index.js'
|
|
196
|
+
)
|
|
197
|
+
).toBe(
|
|
198
|
+
'file:///opt/projects/My%20Project/app/src/my%20components/button.js'
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('handles Unicode characters in paths', () => {
|
|
203
|
+
const base = 'file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app';
|
|
204
|
+
|
|
205
|
+
const importMap: ImportMap = {
|
|
206
|
+
imports: {
|
|
207
|
+
'国际化/':
|
|
208
|
+
'file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app/src/%E5%9B%BD%E9%99%85%E5%8C%96/'
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const resolver = createImportMapResolver(base, importMap);
|
|
213
|
+
|
|
214
|
+
expect(
|
|
215
|
+
resolver(
|
|
216
|
+
'国际化/zh.js',
|
|
217
|
+
'file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app/index.js'
|
|
218
|
+
)
|
|
219
|
+
).toBe(
|
|
220
|
+
'file:///opt/projects/%E5%9B%BD%E9%99%85%E5%8C%96/app/src/%E5%9B%BD%E9%99%85%E5%8C%96/zh.js'
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { parse, resolve } from '@import-maps/resolve';
|
|
3
|
+
import type { ImportMap, ImportMapResolver } from './types';
|
|
4
|
+
|
|
5
|
+
export function createImportMapResolver(
|
|
6
|
+
base: string,
|
|
7
|
+
importMap: ImportMap
|
|
8
|
+
): ImportMapResolver {
|
|
9
|
+
const baseURL = pathToFileURL(base);
|
|
10
|
+
const parsedImportMap = parse(importMap, baseURL);
|
|
11
|
+
return (specifier: string, scriptURL: string): string | null => {
|
|
12
|
+
const result = resolve(specifier, parsedImportMap, new URL(scriptURL));
|
|
13
|
+
if (result.resolvedImport) {
|
|
14
|
+
return result.resolvedImport.href;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
}
|
package/src/import-vm.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { isBuiltin } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import vm from 'node:vm';
|
|
6
|
-
import IM from '@import-maps/resolve';
|
|
7
6
|
import { CircularDependencyError, FileReadError } from './error';
|
|
7
|
+
import { createImportMapResolver } from './import-map-resolve';
|
|
8
8
|
import type { ImportMap } from './types';
|
|
9
9
|
|
|
10
10
|
async function importBuiltinModule(specifier: string, context: vm.Context) {
|
|
@@ -30,22 +30,19 @@ async function importBuiltinModule(specifier: string, context: vm.Context) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function createVmImport(baseURL: URL, importMap: ImportMap = {}) {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const result =
|
|
33
|
+
const importMapResolver = createImportMapResolver(baseURL.href, importMap);
|
|
34
|
+
const buildMeta = (specifier: string, parent: string): ImportMeta => {
|
|
35
|
+
const result = importMapResolver(specifier, parent);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
filename = result.resolvedImport.href;
|
|
40
|
-
} else {
|
|
41
|
-
filename = import.meta.resolve(specifier, parent);
|
|
42
|
-
}
|
|
43
|
-
const url = pathToFileURL(filename);
|
|
44
|
-
const pathname = fileURLToPath(url);
|
|
37
|
+
const url: string = result ?? import.meta.resolve(specifier, parent);
|
|
38
|
+
const filename = fileURLToPath(url);
|
|
45
39
|
return {
|
|
46
40
|
filename,
|
|
41
|
+
dirname: path.dirname(filename),
|
|
47
42
|
url,
|
|
48
|
-
|
|
43
|
+
resolve: (specifier: string, parent: string | URL = url) => {
|
|
44
|
+
return import.meta.resolve(specifier, parent);
|
|
45
|
+
}
|
|
49
46
|
};
|
|
50
47
|
};
|
|
51
48
|
async function moduleLinker(
|
|
@@ -58,17 +55,17 @@ export function createVmImport(baseURL: URL, importMap: ImportMap = {}) {
|
|
|
58
55
|
if (isBuiltin(specifier)) {
|
|
59
56
|
return importBuiltinModule(specifier, context);
|
|
60
57
|
}
|
|
61
|
-
const
|
|
58
|
+
const meta = buildMeta(specifier, parent);
|
|
62
59
|
|
|
63
|
-
if (moduleIds.includes(
|
|
60
|
+
if (moduleIds.includes(meta.filename)) {
|
|
64
61
|
throw new CircularDependencyError(
|
|
65
62
|
'Circular dependency detected',
|
|
66
63
|
moduleIds,
|
|
67
|
-
|
|
64
|
+
meta.filename
|
|
68
65
|
);
|
|
69
66
|
}
|
|
70
67
|
|
|
71
|
-
const module = cache.get(
|
|
68
|
+
const module = cache.get(meta.filename);
|
|
72
69
|
if (module) {
|
|
73
70
|
return module;
|
|
74
71
|
}
|
|
@@ -78,53 +75,44 @@ export function createVmImport(baseURL: URL, importMap: ImportMap = {}) {
|
|
|
78
75
|
});
|
|
79
76
|
});
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
cache.set(parsed.pathname, modulePromise);
|
|
78
|
+
cache.set(meta.filename, modulePromise);
|
|
83
79
|
return modulePromise;
|
|
84
80
|
|
|
85
81
|
async function moduleBuild(): Promise<vm.SourceTextModule> {
|
|
86
82
|
let text: string;
|
|
87
83
|
try {
|
|
88
|
-
text = fs.readFileSync(
|
|
84
|
+
text = fs.readFileSync(meta.filename, 'utf-8');
|
|
89
85
|
} catch (error) {
|
|
90
86
|
throw new FileReadError(
|
|
91
|
-
`Failed to read module: ${
|
|
87
|
+
`Failed to read module: ${meta.filename}`,
|
|
92
88
|
moduleIds,
|
|
93
|
-
|
|
89
|
+
meta.filename,
|
|
94
90
|
error as Error
|
|
95
91
|
);
|
|
96
92
|
}
|
|
97
93
|
const module = new vm.SourceTextModule(text, {
|
|
98
|
-
initializeImportMeta: (
|
|
99
|
-
|
|
100
|
-
meta.dirname = dirname;
|
|
101
|
-
meta.resolve = (
|
|
102
|
-
specifier: string,
|
|
103
|
-
parent: string | URL = parsed.url
|
|
104
|
-
) => {
|
|
105
|
-
return import.meta.resolve(specifier, parent);
|
|
106
|
-
};
|
|
107
|
-
meta.url = parsed.url.toString();
|
|
94
|
+
initializeImportMeta: (importMeta) => {
|
|
95
|
+
Object.assign(importMeta, meta);
|
|
108
96
|
},
|
|
109
97
|
identifier: specifier,
|
|
110
98
|
context: context,
|
|
111
99
|
importModuleDynamically: (specifier, referrer) => {
|
|
112
100
|
return moduleLinker(
|
|
113
101
|
specifier,
|
|
114
|
-
|
|
102
|
+
meta.filename,
|
|
115
103
|
referrer.context,
|
|
116
104
|
cache,
|
|
117
|
-
[...moduleIds,
|
|
105
|
+
[...moduleIds, meta.filename]
|
|
118
106
|
);
|
|
119
107
|
}
|
|
120
108
|
});
|
|
121
109
|
await module.link((specifier: string, referrer) => {
|
|
122
110
|
return moduleLinker(
|
|
123
111
|
specifier,
|
|
124
|
-
|
|
112
|
+
meta.filename,
|
|
125
113
|
referrer.context,
|
|
126
114
|
cache,
|
|
127
|
-
[...moduleIds,
|
|
115
|
+
[...moduleIds, meta.filename]
|
|
128
116
|
);
|
|
129
117
|
});
|
|
130
118
|
await module.evaluate();
|