@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 CHANGED
@@ -15,7 +15,7 @@ npm i @esm.sh/import-map
15
15
 
16
16
  ## API
17
17
 
18
- ### `new ImportMap(baseURL?: string, raw?: ImportMapRaw)`
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("https://example.com/app/", {
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 = getCdnOrigin(config.cdn);
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 = `${specifierOf(imp)}${SPECIFIER_MARK_SEPARATOR}${imp.version}`;
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 = `${cdnOrigin}/`;
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(`${cdnOrigin}/`)) {
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
- `incorrect peer dependency(unmeet ${depImport.version}): ${depImport.name}@${existingVersion}`
92
+ "incorrect peer dependency(unmeet " + depImport.version + "): " + depImport.name + "@" + existingVersion
91
93
  );
92
94
  return;
93
95
  }
94
- const scope = `${cdnOrigin}/${esmSpecifierOf(imp)}/`;
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 = `${scopeName}/${imp.name}`;
164
+ imp.name = scopeName + "/" + imp.name;
163
165
  }
164
166
  if (!imp.name) {
165
- throw new Error(`invalid package name or version: ${specifier}`);
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 getCdnOrigin(cdn) {
177
+ function normalizeCdnOrigin(cdn) {
176
178
  if (cdn && (cdn.startsWith("https://") || cdn.startsWith("http://"))) {
177
- return cdn.replace(/\/+$/, "");
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 `${prefix}${imp.name}${imp.subPath ? `/${imp.subPath}` : ""}`;
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 `${prefix}${external}${imp.name}@${imp.version}`;
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(`/${meta.name}@`)) {
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 = `${cdnOrigin}/${esmSpecifierOf(imp)}/${target}/`;
217
+ let url = cdnOrigin + "/" + esmSpecifierOf(imp) + "/" + target + "/";
212
218
  if (imp.subPath) {
213
219
  if (imp.dev || imp.subPath === "jsx-dev-runtime") {
214
- url += `${imp.subPath}.development.mjs`;
220
+ url += imp.subPath + ".development.mjs";
215
221
  } else {
216
- url += `${imp.subPath}.mjs`;
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 `${url}${fileName}.mjs`;
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 ? `@${imp.version}` : "";
226
- const subPath = imp.subPath ? `/${imp.subPath}` : "";
227
- const targetQuery = target !== "es2022" ? `&target=${encodeURIComponent(target)}` : "";
228
- const url = `${cdnOrigin}/${star}${registryPrefix(imp)}${imp.name}${version}${subPath}?meta${targetQuery}`;
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 fetch(url);
244
+ const res = await fetcher(url);
235
245
  if (res.status === 404) {
236
- throw new Error(`package not found: ${imp.name}${version}${subPath}`);
246
+ throw new Error("package not found: " + imp.name + version + subPath);
237
247
  }
238
248
  if (!res.ok) {
239
- throw new Error(`unexpected http status ${res.status}: ${await res.text()}`);
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(`invalid meta response from ${url}: ${bodyText.slice(0, 200)}`);
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(`invalid pathname or url: ${pathnameOrUrl}`);
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(`invalid pathname: ${pathnameOrUrl}`);
337
+ throw new Error("invalid pathname: " + pathnameOrUrl);
314
338
  }
315
339
  if (segs[0].startsWith("@")) {
316
340
  if (!segs[1]) {
317
- throw new Error(`invalid pathname: ${pathnameOrUrl}`);
341
+ throw new Error("invalid pathname: " + pathnameOrUrl);
318
342
  }
319
343
  const [name, version] = splitByLast(segs[1], "@");
320
- imp.name = `${segs[0]}/${name}`.replace(/^\*/, "");
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.replace(/^\*/, "");
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(`/${subPath}`)) {
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(baseURL, init) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esm.sh/import-map",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A import map parser and resolver.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
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(baseURL?: string | URL, init?: Record<string, any>);
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;