@esmx/import 3.0.0-rc.44 → 3.0.0-rc.46

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.
@@ -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: Record<string, any>, nextResolve: Function): any;
8
+ export declare function resolve(specifier: string, context: {
9
+ parentURL: string;
10
+ }, nextResolve: Function): any;
9
11
  export {};
@@ -1,6 +1,5 @@
1
1
  import module from "node:module";
2
- import { pathToFileURL } from "node:url";
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 loaderBaseURL = new URL("file:");
29
- let loaderParsedImportMap = {};
27
+ let importMapResolver = null;
30
28
  export function initialize(data) {
31
- loaderBaseURL = new URL(data.baseURL);
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 scriptURL = new URL(context.parentURL);
36
- const result = IM.resolve(specifier, loaderParsedImportMap, scriptURL);
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,2 @@
1
+ import type { ImportMap, ImportMapResolver } from './types';
2
+ export declare function createImportMapResolver(base: string, importMap: ImportMap): ImportMapResolver;
@@ -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
+ });
@@ -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, pathToFileURL } from "node:url";
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 parsedImportMap = IM.parse(importMap, baseURL);
31
- const parse = (specifier, parent) => {
32
- const result = IM.resolve(specifier, parsedImportMap, new URL(parent));
33
- let filename;
34
- if (result.matched && result.resolvedImport) {
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
- pathname
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 parsed = parse(specifier, parent);
52
- if (moduleIds.includes(parsed.pathname)) {
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
- parsed.pathname
53
+ meta.filename
57
54
  );
58
55
  }
59
- const module = cache.get(parsed.pathname);
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
- const dirname = path.dirname(parsed.filename);
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(parsed.pathname, "utf-8");
70
+ text = fs.readFileSync(meta.filename, "utf-8");
75
71
  } catch (error) {
76
72
  throw new FileReadError(
77
- `Failed to read module: ${parsed.pathname}`,
73
+ `Failed to read module: ${meta.filename}`,
78
74
  moduleIds,
79
- parsed.pathname,
75
+ meta.filename,
80
76
  error
81
77
  );
82
78
  }
83
79
  const module2 = new vm.SourceTextModule(text, {
84
- initializeImportMeta: (meta) => {
85
- meta.filename = parsed.filename;
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
- parsed.filename,
88
+ meta.filename,
98
89
  referrer.context,
99
90
  cache,
100
- [...moduleIds, parsed.pathname]
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
- parsed.filename,
98
+ meta.filename,
108
99
  referrer.context,
109
100
  cache,
110
- [...moduleIds, parsed.pathname]
101
+ [...moduleIds, meta.filename]
111
102
  );
112
103
  });
113
104
  await module2.evaluate();
package/dist/types.d.ts CHANGED
@@ -4,3 +4,4 @@ export interface ImportMap {
4
4
  imports?: SpecifierMap;
5
5
  scopes?: ScopesMap;
6
6
  }
7
+ export type ImportMapResolver = (specifier: string, parent: string) => string | null;
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.44",
36
+ "@esmx/lint": "3.0.0-rc.46",
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",
44
+ "version": "3.0.0-rc.46",
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": "c83ac6a9b183e5f1a98884d9ae18ff99aacf1a54"
63
+ "gitHead": "dcac010017466e95e1c7db78094d4925c27d54b9"
64
64
  }
@@ -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 type { ImportMap } from './types';
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 loaderBaseURL: URL = new URL('file:');
38
- let loaderParsedImportMap: IM.ParsedImportMap = {};
37
+ let importMapResolver: ImportMapResolver | null = null;
39
38
 
40
39
  export function initialize(data: Data) {
41
- loaderBaseURL = new URL(data.baseURL);
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: Record<string, any>,
45
+ context: { parentURL: string },
48
46
  nextResolve: Function
49
47
  ) {
50
- const scriptURL = new URL(context.parentURL);
51
- const result = IM.resolve(specifier, loaderParsedImportMap, scriptURL);
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, pathToFileURL } from 'node:url';
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 parsedImportMap = IM.parse(importMap, baseURL);
34
- const parse = (specifier: string, parent: string) => {
35
- const result = IM.resolve(specifier, parsedImportMap, new URL(parent));
33
+ const importMapResolver = createImportMapResolver(baseURL.href, importMap);
34
+ const buildMeta = (specifier: string, parent: string): ImportMeta => {
35
+ const result = importMapResolver(specifier, parent);
36
36
 
37
- let filename: string;
38
- if (result.matched && result.resolvedImport) {
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
- pathname
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 parsed = parse(specifier, parent);
58
+ const meta = buildMeta(specifier, parent);
62
59
 
63
- if (moduleIds.includes(parsed.pathname)) {
60
+ if (moduleIds.includes(meta.filename)) {
64
61
  throw new CircularDependencyError(
65
62
  'Circular dependency detected',
66
63
  moduleIds,
67
- parsed.pathname
64
+ meta.filename
68
65
  );
69
66
  }
70
67
 
71
- const module = cache.get(parsed.pathname);
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
- const dirname = path.dirname(parsed.filename);
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(parsed.pathname, 'utf-8');
84
+ text = fs.readFileSync(meta.filename, 'utf-8');
89
85
  } catch (error) {
90
86
  throw new FileReadError(
91
- `Failed to read module: ${parsed.pathname}`,
87
+ `Failed to read module: ${meta.filename}`,
92
88
  moduleIds,
93
- parsed.pathname,
89
+ meta.filename,
94
90
  error as Error
95
91
  );
96
92
  }
97
93
  const module = new vm.SourceTextModule(text, {
98
- initializeImportMeta: (meta) => {
99
- meta.filename = parsed.filename;
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
- parsed.filename,
102
+ meta.filename,
115
103
  referrer.context,
116
104
  cache,
117
- [...moduleIds, parsed.pathname]
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
- parsed.filename,
112
+ meta.filename,
125
113
  referrer.context,
126
114
  cache,
127
- [...moduleIds, parsed.pathname]
115
+ [...moduleIds, meta.filename]
128
116
  );
129
117
  });
130
118
  await module.evaluate();
package/src/types.ts CHANGED
@@ -6,3 +6,8 @@ export interface ImportMap {
6
6
  imports?: SpecifierMap;
7
7
  scopes?: ScopesMap;
8
8
  }
9
+
10
+ export type ImportMapResolver = (
11
+ specifier: string,
12
+ parent: string
13
+ ) => string | null;