@esm.sh/import-map 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/index.mjs +67 -36
- package/package.json +1 -1
- package/types/index.d.ts +4 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npm i @esm.sh/import-map
|
|
|
15
15
|
|
|
16
16
|
## API
|
|
17
17
|
|
|
18
|
-
### `new ImportMap(
|
|
18
|
+
### `new ImportMap(init?: ImportMapRaw, baseURL?: string)`
|
|
19
19
|
|
|
20
20
|
Create an import map instance:
|
|
21
21
|
|
|
@@ -28,7 +28,7 @@ const im = new ImportMap();
|
|
|
28
28
|
You can also initialize from a raw object:
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
|
-
const im = new ImportMap(
|
|
31
|
+
const im = new ImportMap({
|
|
32
32
|
config: { cdn: "https://esm.sh", target: "es2022" },
|
|
33
33
|
imports: { react: "https://esm.sh/react@19.2.4/es2022/react.mjs" },
|
|
34
34
|
scopes: {
|
|
@@ -99,6 +99,19 @@ const im = new ImportMap();
|
|
|
99
99
|
await im.addImport("react-dom@19/client");
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
### `setFetcher(fetcher: (url: string | URL) => Promise<Response>)`
|
|
103
|
+
|
|
104
|
+
Override the default `fetch` used internally by `addImport`.
|
|
105
|
+
This is useful for caching metadata responses, or to use a custom fetch implementation.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// Use a custom fetch with cache.
|
|
109
|
+
setFetcher(cacheFetch);
|
|
110
|
+
|
|
111
|
+
// Restore default behavior.
|
|
112
|
+
setFetcher(globalThis.fetch);
|
|
113
|
+
```
|
|
114
|
+
|
|
102
115
|
### `ImportMap.resolve(specifier: string, containingFile: string)`
|
|
103
116
|
|
|
104
117
|
The `resolve` method resolves a specifier using import-map matching rules:
|
package/dist/index.mjs
CHANGED
|
@@ -35,18 +35,20 @@ async function addImport(importMap, specifier, noSRI) {
|
|
|
35
35
|
const imp = parseImportSpecifier(specifier);
|
|
36
36
|
const config = importMap.config ?? {};
|
|
37
37
|
const target = normalizeTarget(config.target);
|
|
38
|
-
const cdnOrigin =
|
|
38
|
+
const cdnOrigin = normalizeCdnOrigin(config.cdn);
|
|
39
39
|
const meta = await fetchImportMeta(cdnOrigin, imp, target);
|
|
40
40
|
const mark = /* @__PURE__ */ new Set();
|
|
41
41
|
await addImportImpl(importMap, mark, meta, false, void 0, cdnOrigin, target, noSRI ?? false);
|
|
42
|
+
pruneScopeSpecifiersShadowedByImports(importMap);
|
|
43
|
+
pruneEmptyScopes(importMap);
|
|
42
44
|
}
|
|
43
45
|
async function addImportImpl(importMap, mark, imp, indirect, targetImports, cdnOrigin, target, noSRI) {
|
|
44
|
-
const markedSpecifier =
|
|
46
|
+
const markedSpecifier = specifierOf(imp) + SPECIFIER_MARK_SEPARATOR + imp.version;
|
|
45
47
|
if (mark.has(markedSpecifier)) {
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
50
|
mark.add(markedSpecifier);
|
|
49
|
-
const cdnScopeKey =
|
|
51
|
+
const cdnScopeKey = cdnOrigin + "/";
|
|
50
52
|
const cdnScopeImports = importMap.scopes?.[cdnScopeKey];
|
|
51
53
|
const imports = indirect ? targetImports ?? ensureScope(importMap, cdnScopeKey) : importMap.imports;
|
|
52
54
|
const moduleUrl = moduleUrlOf(cdnOrigin, target, imp);
|
|
@@ -75,7 +77,7 @@ async function addImportImpl(importMap, mark, imp, indirect, targetImports, cdnO
|
|
|
75
77
|
const depSpecifier = specifierOf(depImport);
|
|
76
78
|
const existingUrl = importMap.imports[depSpecifier] ?? importMap.scopes?.[cdnScopeKey]?.[depSpecifier];
|
|
77
79
|
let scopedTargetImports = targetImports;
|
|
78
|
-
if (existingUrl?.startsWith(
|
|
80
|
+
if (existingUrl?.startsWith(cdnOrigin + "/")) {
|
|
79
81
|
const existingImport = parseEsmPath(existingUrl);
|
|
80
82
|
const existingVersion = valid(existingImport.version);
|
|
81
83
|
if (existingVersion && depImport.version === existingImport.version) {
|
|
@@ -87,11 +89,11 @@ async function addImportImpl(importMap, mark, imp, indirect, targetImports, cdnO
|
|
|
87
89
|
}
|
|
88
90
|
if (isPeer) {
|
|
89
91
|
console.warn(
|
|
90
|
-
|
|
92
|
+
"incorrect peer dependency(unmeet " + depImport.version + "): " + depImport.name + "@" + existingVersion
|
|
91
93
|
);
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
|
-
const scope =
|
|
96
|
+
const scope = cdnOrigin + "/" + esmSpecifierOf(imp) + "/";
|
|
95
97
|
scopedTargetImports = ensureScope(importMap, scope);
|
|
96
98
|
}
|
|
97
99
|
}
|
|
@@ -159,10 +161,10 @@ function parseImportSpecifier(specifier) {
|
|
|
159
161
|
[packageAndVersion, imp.subPath] = splitByFirst(source, "/");
|
|
160
162
|
[imp.name, imp.version] = splitByFirst(packageAndVersion, "@");
|
|
161
163
|
if (scopeName) {
|
|
162
|
-
imp.name =
|
|
164
|
+
imp.name = scopeName + "/" + imp.name;
|
|
163
165
|
}
|
|
164
166
|
if (!imp.name) {
|
|
165
|
-
throw new Error(
|
|
167
|
+
throw new Error("invalid package name or version: " + specifier);
|
|
166
168
|
}
|
|
167
169
|
return imp;
|
|
168
170
|
}
|
|
@@ -172,20 +174,24 @@ function normalizeTarget(target) {
|
|
|
172
174
|
}
|
|
173
175
|
return "es2022";
|
|
174
176
|
}
|
|
175
|
-
function
|
|
177
|
+
function normalizeCdnOrigin(cdn) {
|
|
176
178
|
if (cdn && (cdn.startsWith("https://") || cdn.startsWith("http://"))) {
|
|
177
|
-
|
|
179
|
+
try {
|
|
180
|
+
return new URL(cdn).origin;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.warn("invalid cdn: " + cdn);
|
|
183
|
+
}
|
|
178
184
|
}
|
|
179
185
|
return "https://esm.sh";
|
|
180
186
|
}
|
|
181
187
|
function specifierOf(imp) {
|
|
182
188
|
const prefix = imp.github ? "gh:" : imp.jsr ? "jsr:" : "";
|
|
183
|
-
return
|
|
189
|
+
return prefix + imp.name + (imp.subPath ? "/" + imp.subPath : "");
|
|
184
190
|
}
|
|
185
191
|
function esmSpecifierOf(imp) {
|
|
186
192
|
const prefix = imp.github ? "gh/" : imp.jsr ? "jsr/" : "";
|
|
187
193
|
const external = hasExternalImports(imp) ? "*" : "";
|
|
188
|
-
return
|
|
194
|
+
return prefix + external + imp.name + "@" + imp.version;
|
|
189
195
|
}
|
|
190
196
|
function registryPrefix(imp) {
|
|
191
197
|
if (imp.github) {
|
|
@@ -201,49 +207,53 @@ function hasExternalImports(meta) {
|
|
|
201
207
|
return true;
|
|
202
208
|
}
|
|
203
209
|
for (const dep of meta.imports) {
|
|
204
|
-
if (!dep.startsWith("/node/") && !dep.startsWith(
|
|
210
|
+
if (!dep.startsWith("/node/") && !dep.startsWith("/" + meta.name + "@")) {
|
|
205
211
|
return true;
|
|
206
212
|
}
|
|
207
213
|
}
|
|
208
214
|
return false;
|
|
209
215
|
}
|
|
210
216
|
function moduleUrlOf(cdnOrigin, target, imp) {
|
|
211
|
-
let url =
|
|
217
|
+
let url = cdnOrigin + "/" + esmSpecifierOf(imp) + "/" + target + "/";
|
|
212
218
|
if (imp.subPath) {
|
|
213
219
|
if (imp.dev || imp.subPath === "jsx-dev-runtime") {
|
|
214
|
-
url +=
|
|
220
|
+
url += imp.subPath + ".development.mjs";
|
|
215
221
|
} else {
|
|
216
|
-
url +=
|
|
222
|
+
url += imp.subPath + ".mjs";
|
|
217
223
|
}
|
|
218
224
|
return url;
|
|
219
225
|
}
|
|
220
226
|
const fileName = imp.name.includes("/") ? imp.name.split("/").at(-1) : imp.name;
|
|
221
|
-
return
|
|
227
|
+
return url + fileName + ".mjs";
|
|
228
|
+
}
|
|
229
|
+
var fetcher = globalThis.fetch;
|
|
230
|
+
function setFetcher(f) {
|
|
231
|
+
fetcher = f;
|
|
222
232
|
}
|
|
223
233
|
async function fetchImportMeta(cdnOrigin, imp, target) {
|
|
224
234
|
const star = imp.external ? "*" : "";
|
|
225
|
-
const version = imp.version ?
|
|
226
|
-
const subPath = imp.subPath ?
|
|
227
|
-
const targetQuery = target !== "es2022" ?
|
|
228
|
-
const url =
|
|
235
|
+
const version = imp.version ? "@" + imp.version : "";
|
|
236
|
+
const subPath = imp.subPath ? "/" + imp.subPath : "";
|
|
237
|
+
const targetQuery = target !== "es2022" ? "&target=" + encodeURIComponent(target) : "";
|
|
238
|
+
const url = cdnOrigin + "/" + star + registryPrefix(imp) + imp.name + version + subPath + "?meta" + targetQuery;
|
|
229
239
|
const cached = META_CACHE.get(url);
|
|
230
240
|
if (cached) {
|
|
231
241
|
return cached;
|
|
232
242
|
}
|
|
233
243
|
const pending = (async () => {
|
|
234
|
-
const res = await
|
|
244
|
+
const res = await fetcher(url);
|
|
235
245
|
if (res.status === 404) {
|
|
236
|
-
throw new Error(
|
|
246
|
+
throw new Error("package not found: " + imp.name + version + subPath);
|
|
237
247
|
}
|
|
238
248
|
if (!res.ok) {
|
|
239
|
-
throw new Error(
|
|
249
|
+
throw new Error("unexpected http status " + res.status + ": " + await res.text());
|
|
240
250
|
}
|
|
241
251
|
const bodyText = await res.text();
|
|
242
252
|
let data;
|
|
243
253
|
try {
|
|
244
254
|
data = JSON.parse(bodyText);
|
|
245
|
-
} catch {
|
|
246
|
-
throw new Error(
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new Error("invalid meta response from " + url + ": " + String(error));
|
|
247
257
|
}
|
|
248
258
|
return {
|
|
249
259
|
name: data.name ?? imp.name,
|
|
@@ -283,6 +293,20 @@ function pruneEmptyScopes(importMap) {
|
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
295
|
}
|
|
296
|
+
function pruneScopeSpecifiersShadowedByImports(importMap) {
|
|
297
|
+
for (const [scopeKey, scopedImports] of Object.entries(importMap.scopes)) {
|
|
298
|
+
if (scopeKey.startsWith("https://") || scopeKey.startsWith("http://")) {
|
|
299
|
+
const url = new URL(scopeKey);
|
|
300
|
+
if (url.pathname === "/") {
|
|
301
|
+
for (const specifier of Object.keys(scopedImports)) {
|
|
302
|
+
if (specifier in importMap.imports) {
|
|
303
|
+
delete scopedImports[specifier];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
286
310
|
function parseEsmPath(pathnameOrUrl) {
|
|
287
311
|
let pathname;
|
|
288
312
|
if (pathnameOrUrl.startsWith("https://") || pathnameOrUrl.startsWith("http://")) {
|
|
@@ -290,7 +314,7 @@ function parseEsmPath(pathnameOrUrl) {
|
|
|
290
314
|
} else if (pathnameOrUrl.startsWith("/")) {
|
|
291
315
|
pathname = splitByFirst(splitByFirst(pathnameOrUrl, "#")[0], "?")[0];
|
|
292
316
|
} else {
|
|
293
|
-
throw new Error(
|
|
317
|
+
throw new Error("invalid pathname or url: " + pathnameOrUrl);
|
|
294
318
|
}
|
|
295
319
|
const imp = {
|
|
296
320
|
name: "",
|
|
@@ -310,19 +334,19 @@ function parseEsmPath(pathnameOrUrl) {
|
|
|
310
334
|
}
|
|
311
335
|
const segs = pathname.split("/").filter(Boolean);
|
|
312
336
|
if (segs.length === 0) {
|
|
313
|
-
throw new Error(
|
|
337
|
+
throw new Error("invalid pathname: " + pathnameOrUrl);
|
|
314
338
|
}
|
|
315
339
|
if (segs[0].startsWith("@")) {
|
|
316
340
|
if (!segs[1]) {
|
|
317
|
-
throw new Error(
|
|
341
|
+
throw new Error("invalid pathname: " + pathnameOrUrl);
|
|
318
342
|
}
|
|
319
343
|
const [name, version] = splitByLast(segs[1], "@");
|
|
320
|
-
imp.name =
|
|
344
|
+
imp.name = trimLeadingStar(segs[0] + "/" + name);
|
|
321
345
|
imp.version = version;
|
|
322
346
|
segs.splice(0, 2);
|
|
323
347
|
} else {
|
|
324
348
|
const [name, version] = splitByLast(segs[0], "@");
|
|
325
|
-
imp.name = name
|
|
349
|
+
imp.name = trimLeadingStar(name);
|
|
326
350
|
imp.version = version;
|
|
327
351
|
segs.splice(0, 1);
|
|
328
352
|
}
|
|
@@ -341,7 +365,7 @@ function parseEsmPath(pathnameOrUrl) {
|
|
|
341
365
|
subPath = subPath.slice(0, -12);
|
|
342
366
|
imp.dev = true;
|
|
343
367
|
}
|
|
344
|
-
if (subPath.includes("/") || subPath !== imp.name && !imp.name.endsWith(
|
|
368
|
+
if (subPath.includes("/") || subPath !== imp.name && !imp.name.endsWith("/" + subPath)) {
|
|
345
369
|
imp.subPath = subPath;
|
|
346
370
|
}
|
|
347
371
|
} else {
|
|
@@ -350,6 +374,12 @@ function parseEsmPath(pathnameOrUrl) {
|
|
|
350
374
|
}
|
|
351
375
|
return imp;
|
|
352
376
|
}
|
|
377
|
+
function trimLeadingStar(value) {
|
|
378
|
+
if (value.startsWith("*")) {
|
|
379
|
+
return value.slice(1);
|
|
380
|
+
}
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
353
383
|
function splitByFirst(value, separator) {
|
|
354
384
|
const idx = value.indexOf(separator);
|
|
355
385
|
if (idx < 0) {
|
|
@@ -464,7 +494,7 @@ var ImportMap = class {
|
|
|
464
494
|
imports = {};
|
|
465
495
|
scopes = {};
|
|
466
496
|
integrity = {};
|
|
467
|
-
constructor(
|
|
497
|
+
constructor(init, baseURL) {
|
|
468
498
|
this.#baseURL = new URL(baseURL ?? globalThis.location?.href ?? "file:///");
|
|
469
499
|
if (init) {
|
|
470
500
|
this.config = sanitizeStringMap(init.config);
|
|
@@ -527,7 +557,7 @@ function sortScopes(scopes) {
|
|
|
527
557
|
|
|
528
558
|
// src/parse.ts
|
|
529
559
|
function parseFromJson(json, baseURL) {
|
|
530
|
-
const im = new ImportMap(baseURL);
|
|
560
|
+
const im = new ImportMap({}, baseURL);
|
|
531
561
|
const v = JSON.parse(json);
|
|
532
562
|
if (isObject(v)) {
|
|
533
563
|
const { config, imports, scopes, integrity } = v;
|
|
@@ -545,7 +575,7 @@ function parseFromHtml(html, baseURL) {
|
|
|
545
575
|
if (scriptEl) {
|
|
546
576
|
return parseFromJson(scriptEl.textContent, baseURL);
|
|
547
577
|
}
|
|
548
|
-
return new ImportMap(baseURL);
|
|
578
|
+
return new ImportMap({}, baseURL);
|
|
549
579
|
}
|
|
550
580
|
|
|
551
581
|
// src/support.ts
|
|
@@ -556,5 +586,6 @@ export {
|
|
|
556
586
|
ImportMap,
|
|
557
587
|
isSupportImportMap,
|
|
558
588
|
parseFromHtml,
|
|
559
|
-
parseFromJson
|
|
589
|
+
parseFromJson,
|
|
590
|
+
setFetcher
|
|
560
591
|
};
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface ImportMapRaw {
|
|
|
25
25
|
|
|
26
26
|
/** The import map class. */
|
|
27
27
|
export class ImportMap {
|
|
28
|
-
constructor(
|
|
28
|
+
constructor(init?: Record<string, any>, baseURL?: string | URL);
|
|
29
29
|
|
|
30
30
|
/** The config for generating the new import url from CDN. */
|
|
31
31
|
config: ImportMapConfig;
|
|
@@ -66,3 +66,6 @@ export function parseFromHtml(html: string, baseURL?: string): ImportMap;
|
|
|
66
66
|
|
|
67
67
|
/** Check if current browser supports import maps. */
|
|
68
68
|
export function isSupportImportMap(): boolean;
|
|
69
|
+
|
|
70
|
+
/** Set the fetcher to use for fetching import meta. */
|
|
71
|
+
export function setFetcher(fetcher: (url: string | URL) => Promise<Response>): void;
|