@esm.sh/import-map 0.2.0 → 0.2.1

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
@@ -1,12 +1,11 @@
1
1
  # @esm.sh/import-map
2
2
 
3
- An [Import Maps](https://wicg.github.io/import-maps/) manager.
3
+ An [Import Maps](https://wicg.github.io/import-maps/) manager, with features:
4
4
 
5
- - create blank import maps
6
- - parse import maps from JSON/HTML
7
- - resolve specifiers with `imports` and `scopes`
8
- - add npm/jsr/github packages from [esm.sh](https://esm.sh) CDN into an import map
9
- - maintain optional `integrity` metadata
5
+ - Parse import maps from JSON/HTML
6
+ - Resolve specifiers to URLs using import map matching rules
7
+ - Add npm/jsr/github modules from [esm.sh](https://esm.sh) CDN
8
+ - Generate `integrity` entries for added modules
10
9
 
11
10
  ## Installation
12
11
 
package/dist/add.mjs CHANGED
@@ -1,13 +1,4 @@
1
1
  import { satisfies, valid } from "semver";
2
- async function addImport(importMap, specifier, noSRI) {
3
- const imp = parseImportSpecifier(specifier);
4
- const config = importMap.config ?? {};
5
- const target = normalizeTarget(config.target);
6
- const cdnOrigin = getCdnOrigin(config.cdn);
7
- const meta = await fetchImportMeta(cdnOrigin, imp, target);
8
- const mark = /* @__PURE__ */ new Set();
9
- await addImportMeta(importMap, mark, meta, false, void 0, cdnOrigin, target, noSRI ?? false);
10
- }
11
2
  const KNOWN_TARGETS = /* @__PURE__ */ new Set([
12
3
  "es2015",
13
4
  "es2016",
@@ -39,6 +30,111 @@ const ESM_SEGMENTS = /* @__PURE__ */ new Set([
39
30
  ]);
40
31
  const SPECIFIER_MARK_SEPARATOR = "\0";
41
32
  const META_CACHE = /* @__PURE__ */ new Map();
33
+ async function addImport(importMap, specifier, noSRI) {
34
+ const imp = parseImportSpecifier(specifier);
35
+ const config = importMap.config ?? {};
36
+ const target = normalizeTarget(config.target);
37
+ const cdnOrigin = getCdnOrigin(config.cdn);
38
+ const meta = await fetchImportMeta(cdnOrigin, imp, target);
39
+ const mark = /* @__PURE__ */ new Set();
40
+ await addImportImpl(importMap, mark, meta, false, void 0, cdnOrigin, target, noSRI ?? false);
41
+ }
42
+ async function addImportImpl(importMap, mark, imp, indirect, targetImports, cdnOrigin, target, noSRI) {
43
+ const markedSpecifier = `${specifierOf(imp)}${SPECIFIER_MARK_SEPARATOR}${imp.version}`;
44
+ if (mark.has(markedSpecifier)) {
45
+ return;
46
+ }
47
+ mark.add(markedSpecifier);
48
+ const cdnScopeKey = `${cdnOrigin}/`;
49
+ const cdnScopeImports = importMap.scopes?.[cdnScopeKey];
50
+ const imports = indirect ? targetImports ?? ensureScope(importMap, cdnScopeKey) : importMap.imports;
51
+ const moduleUrl = moduleUrlOf(cdnOrigin, target, imp);
52
+ const currentSpecifier = specifierOf(imp);
53
+ imports[currentSpecifier] = moduleUrl;
54
+ await updateIntegrity(importMap, imp, moduleUrl, cdnOrigin, target, noSRI);
55
+ if (!indirect) {
56
+ if (cdnScopeImports) {
57
+ delete cdnScopeImports[currentSpecifier];
58
+ }
59
+ pruneEmptyScopes(importMap);
60
+ }
61
+ const allDeps = [
62
+ ...imp.peerImports.map((pathname) => ({ pathname, isPeer: true })),
63
+ ...imp.imports.map((pathname) => ({ pathname, isPeer: false }))
64
+ ];
65
+ await Promise.all(
66
+ allDeps.map(async ({ pathname, isPeer }) => {
67
+ if (pathname.startsWith("/node/")) {
68
+ return;
69
+ }
70
+ const depImport = parseEsmPath(pathname);
71
+ if (depImport.name === imp.name) {
72
+ depImport.version = imp.version;
73
+ }
74
+ const depSpecifier = specifierOf(depImport);
75
+ const existingUrl = importMap.imports[depSpecifier] ?? importMap.scopes?.[cdnScopeKey]?.[depSpecifier];
76
+ let scopedTargetImports = targetImports;
77
+ if (existingUrl?.startsWith(`${cdnOrigin}/`)) {
78
+ const existingImport = parseEsmPath(existingUrl);
79
+ const existingVersion = valid(existingImport.version);
80
+ if (existingVersion && depImport.version === existingImport.version) {
81
+ return;
82
+ }
83
+ if (existingVersion && depImport.version && !valid(depImport.version)) {
84
+ if (satisfies(existingVersion, depImport.version, { includePrerelease: true })) {
85
+ return;
86
+ }
87
+ if (isPeer) {
88
+ console.warn(
89
+ `incorrect peer dependency(unmeet ${depImport.version}): ${depImport.name}@${existingVersion}`
90
+ );
91
+ return;
92
+ }
93
+ const scope = `${cdnOrigin}/${esmSpecifierOf(imp)}/`;
94
+ scopedTargetImports = ensureScope(importMap, scope);
95
+ }
96
+ }
97
+ const depMeta = await fetchImportMeta(cdnOrigin, depImport, target);
98
+ await addImportImpl(importMap, mark, depMeta, !isPeer, scopedTargetImports, cdnOrigin, target, noSRI);
99
+ })
100
+ );
101
+ pruneEmptyScopes(importMap);
102
+ }
103
+ async function updateIntegrity(importMap, imp, moduleUrl, cdnOrigin, target, noSRI) {
104
+ if (noSRI) {
105
+ if (importMap.integrity) {
106
+ delete importMap.integrity[moduleUrl];
107
+ if (Object.keys(importMap.integrity).length === 0) {
108
+ delete importMap.integrity;
109
+ }
110
+ }
111
+ return;
112
+ }
113
+ if (!hasExternalImports(imp)) {
114
+ if (imp.integrity) {
115
+ importMap.integrity ??= {};
116
+ importMap.integrity[moduleUrl] = imp.integrity;
117
+ }
118
+ return;
119
+ }
120
+ const integrityMeta = await fetchImportMeta(
121
+ cdnOrigin,
122
+ {
123
+ name: imp.name,
124
+ version: imp.version,
125
+ subPath: imp.subPath,
126
+ github: imp.github,
127
+ jsr: imp.jsr,
128
+ external: true,
129
+ dev: imp.dev
130
+ },
131
+ target
132
+ );
133
+ if (integrityMeta.integrity) {
134
+ importMap.integrity ??= {};
135
+ importMap.integrity[moduleUrl] = integrityMeta.integrity;
136
+ }
137
+ }
42
138
  function parseImportSpecifier(specifier) {
43
139
  let source = specifier.trim();
44
140
  const imp = {
@@ -174,102 +270,6 @@ async function fetchImportMeta(cdnOrigin, imp, target) {
174
270
  throw error;
175
271
  }
176
272
  }
177
- async function addImportMeta(importMap, mark, imp, indirect, targetImports, cdnOrigin, target, noSRI) {
178
- const markedSpecifier = `${specifierOf(imp)}${SPECIFIER_MARK_SEPARATOR}${imp.version}`;
179
- if (mark.has(markedSpecifier)) {
180
- return;
181
- }
182
- mark.add(markedSpecifier);
183
- const cdnScopeKey = `${cdnOrigin}/`;
184
- const cdnScopeImports = importMap.scopes?.[cdnScopeKey];
185
- const imports = indirect ? targetImports ?? ensureScope(importMap, cdnScopeKey) : importMap.imports;
186
- const moduleUrl = moduleUrlOf(cdnOrigin, target, imp);
187
- const currentSpecifier = specifierOf(imp);
188
- imports[currentSpecifier] = moduleUrl;
189
- await updateIntegrity(importMap, imp, moduleUrl, cdnOrigin, target, noSRI);
190
- if (!indirect) {
191
- if (cdnScopeImports) {
192
- delete cdnScopeImports[currentSpecifier];
193
- }
194
- pruneEmptyScopes(importMap);
195
- }
196
- const allDeps = [
197
- ...imp.peerImports.map((pathname) => ({ pathname, isPeer: true })),
198
- ...imp.imports.map((pathname) => ({ pathname, isPeer: false }))
199
- ];
200
- await Promise.all(
201
- allDeps.map(async ({ pathname, isPeer }) => {
202
- if (pathname.startsWith("/node/")) {
203
- return;
204
- }
205
- const depImport = parseEsmPath(pathname);
206
- if (depImport.name === imp.name) {
207
- depImport.version = imp.version;
208
- }
209
- const depSpecifier = specifierOf(depImport);
210
- const existingUrl = importMap.imports[depSpecifier] ?? importMap.scopes?.[cdnScopeKey]?.[depSpecifier];
211
- let scopedTargetImports = targetImports;
212
- if (existingUrl?.startsWith(`${cdnOrigin}/`)) {
213
- const existingImport = parseEsmPath(existingUrl);
214
- const existingVersion = valid(existingImport.version);
215
- if (existingVersion && depImport.version === existingImport.version) {
216
- return;
217
- }
218
- if (existingVersion && depImport.version && !valid(depImport.version)) {
219
- if (satisfies(existingVersion, depImport.version, { includePrerelease: true })) {
220
- return;
221
- }
222
- if (isPeer) {
223
- console.warn(
224
- `incorrect peer dependency(unmeet ${depImport.version}): ${depImport.name}@${existingVersion}`
225
- );
226
- return;
227
- }
228
- const scope = `${cdnOrigin}/${esmSpecifierOf(imp)}/`;
229
- scopedTargetImports = ensureScope(importMap, scope);
230
- }
231
- }
232
- const depMeta = await fetchImportMeta(cdnOrigin, depImport, target);
233
- await addImportMeta(importMap, mark, depMeta, !isPeer, scopedTargetImports, cdnOrigin, target, noSRI);
234
- })
235
- );
236
- pruneEmptyScopes(importMap);
237
- }
238
- async function updateIntegrity(importMap, imp, moduleUrl, cdnOrigin, target, noSRI) {
239
- if (noSRI) {
240
- if (importMap.integrity) {
241
- delete importMap.integrity[moduleUrl];
242
- if (Object.keys(importMap.integrity).length === 0) {
243
- delete importMap.integrity;
244
- }
245
- }
246
- return;
247
- }
248
- if (!hasExternalImports(imp)) {
249
- if (imp.integrity) {
250
- importMap.integrity ??= {};
251
- importMap.integrity[moduleUrl] = imp.integrity;
252
- }
253
- return;
254
- }
255
- const integrityMeta = await fetchImportMeta(
256
- cdnOrigin,
257
- {
258
- name: imp.name,
259
- version: imp.version,
260
- subPath: imp.subPath,
261
- github: imp.github,
262
- jsr: imp.jsr,
263
- external: true,
264
- dev: imp.dev
265
- },
266
- target
267
- );
268
- if (integrityMeta.integrity) {
269
- importMap.integrity ??= {};
270
- importMap.integrity[moduleUrl] = integrityMeta.integrity;
271
- }
272
- }
273
273
  function ensureScope(importMap, scopeKey) {
274
274
  importMap.scopes ??= {};
275
275
  importMap.scopes[scopeKey] ??= {};
package/dist/blank.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  function createBlankImportMap(baseURL) {
2
2
  return {
3
- baseURL: new URL(baseURL ?? ".", "file:///"),
3
+ baseURL: baseURL ? new URL(baseURL, "file:///") : void 0,
4
4
  imports: {}
5
5
  };
6
6
  }
package/dist/resolve.mjs CHANGED
@@ -1,45 +1,63 @@
1
1
  function resolve(importMap, specifier, containingFile) {
2
- const { baseURL, imports, scopes } = importMap;
3
- const { origin, pathname } = new URL(containingFile, baseURL);
4
- const sameOriginScopes = [];
5
- for (const scopeName in scopes ?? {}) {
6
- const scopeUrl = new URL(scopeName, baseURL);
7
- if (scopeUrl.origin === origin) {
8
- sameOriginScopes.push([scopeUrl.pathname, (scopes ?? {})[scopeName]]);
2
+ const baseURL = importMap.baseURL ?? new URL(globalThis.location?.href ?? "file:///");
3
+ const referrer = new URL(containingFile, baseURL);
4
+ const [specifierWithoutHash, hashPart = ""] = specifier.split("#", 2);
5
+ const [specifierWithoutQuery, queryPart = ""] = specifierWithoutHash.split("?", 2);
6
+ const hash = hashPart ? `#${hashPart}` : "";
7
+ const query = queryPart ? `?${queryPart}` : "";
8
+ const cleanSpecifier = specifierWithoutQuery;
9
+ const scopes = importMap.scopes ?? {};
10
+ const scopeEntries = Object.entries(scopes).map(([scopeKey, scopeImports]) => {
11
+ try {
12
+ return [new URL(scopeKey, baseURL).toString(), scopeImports];
13
+ } catch {
14
+ return [scopeKey, scopeImports];
9
15
  }
10
- }
11
- sameOriginScopes.sort(([a], [b]) => b.split("/").length - a.split("/").length);
12
- if (sameOriginScopes.length > 0) {
13
- for (const [scopePathname, scopeImports] of sameOriginScopes) {
14
- if (pathname.startsWith(scopePathname)) {
15
- const url = matchImport(specifier, scopeImports);
16
- if (url) {
17
- return [url, true];
18
- }
19
- }
16
+ }).sort((a, b) => compareScopeKeys(a[0], b[0]));
17
+ for (const [scopeKey, scopeImports] of scopeEntries) {
18
+ if (!referrer.toString().startsWith(scopeKey)) {
19
+ continue;
20
20
  }
21
- }
22
- if (origin === baseURL.origin) {
23
- const url = matchImport(specifier, imports);
24
- if (url) {
25
- return [url, true];
21
+ const mapped2 = resolveWith(cleanSpecifier, scopeImports ?? {});
22
+ if (mapped2) {
23
+ return [normalizeUrl(baseURL, mapped2) + query + hash, true];
26
24
  }
27
25
  }
28
- return [specifier, false];
26
+ const mapped = resolveWith(cleanSpecifier, importMap.imports);
27
+ if (mapped) {
28
+ return [normalizeUrl(baseURL, mapped) + query + hash, true];
29
+ }
30
+ return [cleanSpecifier + query + hash, false];
29
31
  }
30
- function matchImport(specifier, imports) {
31
- if (specifier in imports) {
32
+ function resolveWith(specifier, imports) {
33
+ if (imports[specifier]) {
32
34
  return imports[specifier];
33
35
  }
34
- let bestMatch = null;
35
- let bestKeyLength = -1;
36
- for (const [k, v] of Object.entries(imports)) {
37
- if (k.endsWith("/") && specifier.startsWith(k) && k.length > bestKeyLength) {
38
- bestMatch = v + specifier.slice(k.length);
39
- bestKeyLength = k.length;
36
+ if (!specifier.includes("/")) {
37
+ return null;
38
+ }
39
+ const prefixKeys = Object.keys(imports).filter((k) => k.endsWith("/") && specifier.startsWith(k)).sort((a, b) => b.length - a.length || (a < b ? 1 : -1));
40
+ for (const key of prefixKeys) {
41
+ const value = imports[key];
42
+ if (value && value.endsWith("/")) {
43
+ return value + specifier.slice(key.length);
40
44
  }
41
45
  }
42
- return bestMatch;
46
+ return null;
47
+ }
48
+ function compareScopeKeys(a, b) {
49
+ const aSlashCount = a.split("/").length;
50
+ const bSlashCount = b.split("/").length;
51
+ if (aSlashCount !== bSlashCount) {
52
+ return bSlashCount - aSlashCount;
53
+ }
54
+ return a < b ? 1 : -1;
55
+ }
56
+ function normalizeUrl(baseURL, path) {
57
+ if (path.startsWith("/") || path.startsWith("./") || path.startsWith("../")) {
58
+ return new URL(path, baseURL).toString();
59
+ }
60
+ return path;
43
61
  }
44
62
  export {
45
63
  resolve
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esm.sh/import-map",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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
@@ -1,6 +1,6 @@
1
1
  /** The import maps follow the spec at https://wicg.github.io/import-maps/. */
2
2
  export interface ImportMap {
3
- baseURL: URL;
3
+ baseURL?: URL;
4
4
  config?: Record<string, string>;
5
5
  imports: Record<string, string>;
6
6
  scopes?: Record<string, Record<string, string>>;