@esm.sh/import-map 0.3.2 → 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,7 +35,7 @@ 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);
@@ -43,12 +43,12 @@ async function addImport(importMap, specifier, noSRI) {
43
43
  pruneEmptyScopes(importMap);
44
44
  }
45
45
  async function addImportImpl(importMap, mark, imp, indirect, targetImports, cdnOrigin, target, noSRI) {
46
- const markedSpecifier = `${specifierOf(imp)}${SPECIFIER_MARK_SEPARATOR}${imp.version}`;
46
+ const markedSpecifier = specifierOf(imp) + SPECIFIER_MARK_SEPARATOR + imp.version;
47
47
  if (mark.has(markedSpecifier)) {
48
48
  return;
49
49
  }
50
50
  mark.add(markedSpecifier);
51
- const cdnScopeKey = `${cdnOrigin}/`;
51
+ const cdnScopeKey = cdnOrigin + "/";
52
52
  const cdnScopeImports = importMap.scopes?.[cdnScopeKey];
53
53
  const imports = indirect ? targetImports ?? ensureScope(importMap, cdnScopeKey) : importMap.imports;
54
54
  const moduleUrl = moduleUrlOf(cdnOrigin, target, imp);
@@ -77,7 +77,7 @@ async function addImportImpl(importMap, mark, imp, indirect, targetImports, cdnO
77
77
  const depSpecifier = specifierOf(depImport);
78
78
  const existingUrl = importMap.imports[depSpecifier] ?? importMap.scopes?.[cdnScopeKey]?.[depSpecifier];
79
79
  let scopedTargetImports = targetImports;
80
- if (existingUrl?.startsWith(`${cdnOrigin}/`)) {
80
+ if (existingUrl?.startsWith(cdnOrigin + "/")) {
81
81
  const existingImport = parseEsmPath(existingUrl);
82
82
  const existingVersion = valid(existingImport.version);
83
83
  if (existingVersion && depImport.version === existingImport.version) {
@@ -89,11 +89,11 @@ async function addImportImpl(importMap, mark, imp, indirect, targetImports, cdnO
89
89
  }
90
90
  if (isPeer) {
91
91
  console.warn(
92
- `incorrect peer dependency(unmeet ${depImport.version}): ${depImport.name}@${existingVersion}`
92
+ "incorrect peer dependency(unmeet " + depImport.version + "): " + depImport.name + "@" + existingVersion
93
93
  );
94
94
  return;
95
95
  }
96
- const scope = `${cdnOrigin}/${esmSpecifierOf(imp)}/`;
96
+ const scope = cdnOrigin + "/" + esmSpecifierOf(imp) + "/";
97
97
  scopedTargetImports = ensureScope(importMap, scope);
98
98
  }
99
99
  }
@@ -161,10 +161,10 @@ function parseImportSpecifier(specifier) {
161
161
  [packageAndVersion, imp.subPath] = splitByFirst(source, "/");
162
162
  [imp.name, imp.version] = splitByFirst(packageAndVersion, "@");
163
163
  if (scopeName) {
164
- imp.name = `${scopeName}/${imp.name}`;
164
+ imp.name = scopeName + "/" + imp.name;
165
165
  }
166
166
  if (!imp.name) {
167
- throw new Error(`invalid package name or version: ${specifier}`);
167
+ throw new Error("invalid package name or version: " + specifier);
168
168
  }
169
169
  return imp;
170
170
  }
@@ -174,20 +174,24 @@ function normalizeTarget(target) {
174
174
  }
175
175
  return "es2022";
176
176
  }
177
- function getCdnOrigin(cdn) {
177
+ function normalizeCdnOrigin(cdn) {
178
178
  if (cdn && (cdn.startsWith("https://") || cdn.startsWith("http://"))) {
179
- return cdn.replace(/\/+$/, "");
179
+ try {
180
+ return new URL(cdn).origin;
181
+ } catch (error) {
182
+ console.warn("invalid cdn: " + cdn);
183
+ }
180
184
  }
181
185
  return "https://esm.sh";
182
186
  }
183
187
  function specifierOf(imp) {
184
188
  const prefix = imp.github ? "gh:" : imp.jsr ? "jsr:" : "";
185
- return `${prefix}${imp.name}${imp.subPath ? `/${imp.subPath}` : ""}`;
189
+ return prefix + imp.name + (imp.subPath ? "/" + imp.subPath : "");
186
190
  }
187
191
  function esmSpecifierOf(imp) {
188
192
  const prefix = imp.github ? "gh/" : imp.jsr ? "jsr/" : "";
189
193
  const external = hasExternalImports(imp) ? "*" : "";
190
- return `${prefix}${external}${imp.name}@${imp.version}`;
194
+ return prefix + external + imp.name + "@" + imp.version;
191
195
  }
192
196
  function registryPrefix(imp) {
193
197
  if (imp.github) {
@@ -203,49 +207,53 @@ function hasExternalImports(meta) {
203
207
  return true;
204
208
  }
205
209
  for (const dep of meta.imports) {
206
- if (!dep.startsWith("/node/") && !dep.startsWith(`/${meta.name}@`)) {
210
+ if (!dep.startsWith("/node/") && !dep.startsWith("/" + meta.name + "@")) {
207
211
  return true;
208
212
  }
209
213
  }
210
214
  return false;
211
215
  }
212
216
  function moduleUrlOf(cdnOrigin, target, imp) {
213
- let url = `${cdnOrigin}/${esmSpecifierOf(imp)}/${target}/`;
217
+ let url = cdnOrigin + "/" + esmSpecifierOf(imp) + "/" + target + "/";
214
218
  if (imp.subPath) {
215
219
  if (imp.dev || imp.subPath === "jsx-dev-runtime") {
216
- url += `${imp.subPath}.development.mjs`;
220
+ url += imp.subPath + ".development.mjs";
217
221
  } else {
218
- url += `${imp.subPath}.mjs`;
222
+ url += imp.subPath + ".mjs";
219
223
  }
220
224
  return url;
221
225
  }
222
226
  const fileName = imp.name.includes("/") ? imp.name.split("/").at(-1) : imp.name;
223
- return `${url}${fileName}.mjs`;
227
+ return url + fileName + ".mjs";
228
+ }
229
+ var fetcher = globalThis.fetch;
230
+ function setFetcher(f) {
231
+ fetcher = f;
224
232
  }
225
233
  async function fetchImportMeta(cdnOrigin, imp, target) {
226
234
  const star = imp.external ? "*" : "";
227
- const version = imp.version ? `@${imp.version}` : "";
228
- const subPath = imp.subPath ? `/${imp.subPath}` : "";
229
- const targetQuery = target !== "es2022" ? `&target=${encodeURIComponent(target)}` : "";
230
- 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;
231
239
  const cached = META_CACHE.get(url);
232
240
  if (cached) {
233
241
  return cached;
234
242
  }
235
243
  const pending = (async () => {
236
- const res = await fetch(url);
244
+ const res = await fetcher(url);
237
245
  if (res.status === 404) {
238
- throw new Error(`package not found: ${imp.name}${version}${subPath}`);
246
+ throw new Error("package not found: " + imp.name + version + subPath);
239
247
  }
240
248
  if (!res.ok) {
241
- throw new Error(`unexpected http status ${res.status}: ${await res.text()}`);
249
+ throw new Error("unexpected http status " + res.status + ": " + await res.text());
242
250
  }
243
251
  const bodyText = await res.text();
244
252
  let data;
245
253
  try {
246
254
  data = JSON.parse(bodyText);
247
- } catch {
248
- 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));
249
257
  }
250
258
  return {
251
259
  name: data.name ?? imp.name,
@@ -306,7 +314,7 @@ function parseEsmPath(pathnameOrUrl) {
306
314
  } else if (pathnameOrUrl.startsWith("/")) {
307
315
  pathname = splitByFirst(splitByFirst(pathnameOrUrl, "#")[0], "?")[0];
308
316
  } else {
309
- throw new Error(`invalid pathname or url: ${pathnameOrUrl}`);
317
+ throw new Error("invalid pathname or url: " + pathnameOrUrl);
310
318
  }
311
319
  const imp = {
312
320
  name: "",
@@ -326,19 +334,19 @@ function parseEsmPath(pathnameOrUrl) {
326
334
  }
327
335
  const segs = pathname.split("/").filter(Boolean);
328
336
  if (segs.length === 0) {
329
- throw new Error(`invalid pathname: ${pathnameOrUrl}`);
337
+ throw new Error("invalid pathname: " + pathnameOrUrl);
330
338
  }
331
339
  if (segs[0].startsWith("@")) {
332
340
  if (!segs[1]) {
333
- throw new Error(`invalid pathname: ${pathnameOrUrl}`);
341
+ throw new Error("invalid pathname: " + pathnameOrUrl);
334
342
  }
335
343
  const [name, version] = splitByLast(segs[1], "@");
336
- imp.name = `${segs[0]}/${name}`.replace(/^\*/, "");
344
+ imp.name = trimLeadingStar(segs[0] + "/" + name);
337
345
  imp.version = version;
338
346
  segs.splice(0, 2);
339
347
  } else {
340
348
  const [name, version] = splitByLast(segs[0], "@");
341
- imp.name = name.replace(/^\*/, "");
349
+ imp.name = trimLeadingStar(name);
342
350
  imp.version = version;
343
351
  segs.splice(0, 1);
344
352
  }
@@ -357,7 +365,7 @@ function parseEsmPath(pathnameOrUrl) {
357
365
  subPath = subPath.slice(0, -12);
358
366
  imp.dev = true;
359
367
  }
360
- if (subPath.includes("/") || subPath !== imp.name && !imp.name.endsWith(`/${subPath}`)) {
368
+ if (subPath.includes("/") || subPath !== imp.name && !imp.name.endsWith("/" + subPath)) {
361
369
  imp.subPath = subPath;
362
370
  }
363
371
  } else {
@@ -366,6 +374,12 @@ function parseEsmPath(pathnameOrUrl) {
366
374
  }
367
375
  return imp;
368
376
  }
377
+ function trimLeadingStar(value) {
378
+ if (value.startsWith("*")) {
379
+ return value.slice(1);
380
+ }
381
+ return value;
382
+ }
369
383
  function splitByFirst(value, separator) {
370
384
  const idx = value.indexOf(separator);
371
385
  if (idx < 0) {
@@ -480,7 +494,7 @@ var ImportMap = class {
480
494
  imports = {};
481
495
  scopes = {};
482
496
  integrity = {};
483
- constructor(baseURL, init) {
497
+ constructor(init, baseURL) {
484
498
  this.#baseURL = new URL(baseURL ?? globalThis.location?.href ?? "file:///");
485
499
  if (init) {
486
500
  this.config = sanitizeStringMap(init.config);
@@ -543,7 +557,7 @@ function sortScopes(scopes) {
543
557
 
544
558
  // src/parse.ts
545
559
  function parseFromJson(json, baseURL) {
546
- const im = new ImportMap(baseURL);
560
+ const im = new ImportMap({}, baseURL);
547
561
  const v = JSON.parse(json);
548
562
  if (isObject(v)) {
549
563
  const { config, imports, scopes, integrity } = v;
@@ -561,7 +575,7 @@ function parseFromHtml(html, baseURL) {
561
575
  if (scriptEl) {
562
576
  return parseFromJson(scriptEl.textContent, baseURL);
563
577
  }
564
- return new ImportMap(baseURL);
578
+ return new ImportMap({}, baseURL);
565
579
  }
566
580
 
567
581
  // src/support.ts
@@ -572,5 +586,6 @@ export {
572
586
  ImportMap,
573
587
  isSupportImportMap,
574
588
  parseFromHtml,
575
- parseFromJson
589
+ parseFromJson,
590
+ setFetcher
576
591
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esm.sh/import-map",
3
- "version": "0.3.2",
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;