Conversation
There was a problem hiding this comment.
Pull request overview
Refactors src/add.ts to reorganize fetch/meta handling and parsing logic for esm.sh import specifiers/paths as part of the import-map “add import from CDN” workflow.
Changes:
- Makes
ImportInfo/ImportMetafields optional and updates dependency iteration to handle missingimports/peerImports. - Refactors fetch/meta caching and introduces
META_CACHE_MEMOplus a module-level overridable fetcher viasetFetcher. - Reworks specifier/path parsing and extends registry prefix handling (including a new
pr://pr/registry prefix).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/add.ts
Outdated
| } else if (source.startsWith("pr:")) { | ||
| imp.pr = true; | ||
| source = source.slice(3); | ||
| } |
There was a problem hiding this comment.
pr: is parsed and registryPrefix() supports it, but specifierOf() / esmSpecifierOf() don’t include a pr:/pr/ prefix and fetchImportMeta() doesn’t propagate pr onto the returned ImportMeta. As a result, pr: imports can collide with npm imports and may lose their registry identity after the first meta fetch; please thread pr through and include it in specifier/url prefix generation consistently.
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | ||
| imp.name = pkgNameNoScope; | ||
| imp.version = pkgVersion; | ||
| hasTargetSegment = ESM_TARGETS.has(segs[1]); | ||
| subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/"); |
There was a problem hiding this comment.
GitHub/PR packages are encoded in URLs as /<registry>/<owner>/<repo>@<ver>/... (because imp.name contains a /), but this parser treats only segs[0] as the package segment. For /gh/owner/repo@tag/... this will set name="owner" and push the rest into subPath, producing incorrect specifiers and version comparisons; parseEsmPath should special-case imp.github/imp.pr to combine the first two segments into <owner>/<repo> + version (and then parse target/subpath).
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | |
| imp.name = pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[1]); | |
| subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/"); | |
| if (imp.github || imp.pr) { | |
| const seg1 = segs[1]; | |
| if (!seg1) { | |
| throw new Error("invalid pathname: " + pathnameOrUrl); | |
| } | |
| [pkgNameNoScope, pkgVersion] = seg1.split("@", 2); | |
| imp.name = seg0 + "/" + pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[2]); | |
| subPath = segs.slice(hasTargetSegment ? 3 : 2).join("/"); | |
| } else { | |
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | |
| imp.name = pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[1]); | |
| subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/"); | |
| } |
| let [maybePkgNameAndVersion, ...subPath] = source.split("/"); | ||
| let [pkgNameNoScope, pkgVersion] = maybePkgNameAndVersion.split("@", 2); | ||
| if (scopeName) { | ||
| imp.name = scopeName + "/" + pkgNameNoScope; | ||
| imp.version = pkgVersion; | ||
| } else { | ||
| imp.name = pkgNameNoScope; | ||
| imp.version = pkgVersion; | ||
| } |
There was a problem hiding this comment.
pkgVersion can be undefined when the specifier has no @version (e.g. react), but it’s assigned into imp.version (typed as string). This can later produce URLs/specifiers containing the string "undefined" and breaks type expectations; default pkgVersion to an empty string or explicitly validate/throw when missing.
| [pkgNameNoScope, pkgVersion] = segs[1].split("@", 2); | ||
| imp.name = seg0 + "/" + pkgNameNoScope; | ||
| imp.version = pkgVersion; | ||
| hasTargetSegment = ESM_TARGETS.has(segs[2]); | ||
| subPath = segs.slice(hasTargetSegment ? 3 : 2).join("/"); | ||
| } else { | ||
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); |
There was a problem hiding this comment.
parseEsmPath splits segs[1] by @ and assigns pkgVersion into imp.version, but pkgVersion can be undefined when there is no @ in the segment. This can propagate an undefined version into later URL/specifier generation; consider defaulting to "" (matching previous behavior) or throwing on missing versions.
| [pkgNameNoScope, pkgVersion] = segs[1].split("@", 2); | |
| imp.name = seg0 + "/" + pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[2]); | |
| subPath = segs.slice(hasTargetSegment ? 3 : 2).join("/"); | |
| } else { | |
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | |
| const scopedParts = segs[1].split("@", 2); | |
| pkgNameNoScope = scopedParts[0]; | |
| pkgVersion = scopedParts[1] ?? ""; | |
| imp.name = seg0 + "/" + pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[2]); | |
| subPath = segs.slice(hasTargetSegment ? 3 : 2).join("/"); | |
| } else { | |
| const unscopedParts = seg0.split("@", 2); | |
| pkgNameNoScope = unscopedParts[0]; | |
| pkgVersion = unscopedParts[1] ?? ""; |
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | ||
| imp.name = pkgNameNoScope; | ||
| imp.version = pkgVersion; | ||
| hasTargetSegment = ESM_TARGETS.has(segs[1]); | ||
| subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/"); |
There was a problem hiding this comment.
parseEsmPath splits seg0 by @ and assigns pkgVersion into imp.version, but pkgVersion can be undefined when there is no @ in the segment. This can propagate an undefined version into later URL/specifier generation; consider defaulting to "" (matching previous behavior) or throwing on missing versions.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (imp.github) { | ||
| prefix = "github:"; | ||
| } else if (imp.jsr) { | ||
| prefix = "jsr:"; | ||
| } else if (imp.pr) { | ||
| prefix = "pr:"; | ||
| } |
There was a problem hiding this comment.
specifierOf now canonicalizes GitHub specifiers to github:. This breaks the documented/public specifier form (gh:owner/repo@tag) and can lead to duplicate keys (e.g., user passes gh:... but the import map key becomes github:..., so existing gh: entries won’t be found). Consider keeping gh: as the canonical output (optionally still accepting github: as an input alias), or update the public API/docs consistently.
| github: imp.github, | ||
| jsr: imp.jsr, | ||
| pr: imp.pr, | ||
| external: imp.external, | ||
| dev: imp.dev, |
There was a problem hiding this comment.
pr: imp.pr is propagated into the returned ImportMeta, but updateIntegrity()’s follow-up fetchImportMeta call for external: true doesn’t include pr. For pr: packages that require the external-meta integrity lookup, this will fetch from the wrong registry path and can produce incorrect integrity entries. Ensure the integrity lookup passes pr: imp.pr as well (and consider adding a test for this path).
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | ||
| imp.name = pkgNameNoScope; | ||
| imp.version = pkgVersion; | ||
| hasTargetSegment = ESM_TARGETS.has(segs[1]); | ||
| subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/"); |
There was a problem hiding this comment.
parseEsmPath doesn’t correctly parse GitHub CDN paths like /gh/owner/repo@tag/... where the package name spans two path segments. In the non-scoped branch, seg0 becomes owner (no version), so imp.name/imp.version end up wrong and subPath becomes repo@tag/.... Consider a special-case when imp.github is true (and seg0 is not scoped) to combine segs[0] + / + segs[1] (splitting version from segs[1]) before checking for the target segment.
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | |
| imp.name = pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[1]); | |
| subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/"); | |
| if (imp.github) { | |
| if (!segs[1]) { | |
| throw new Error("invalid pathname: " + pathnameOrUrl); | |
| } | |
| [pkgNameNoScope, pkgVersion] = segs[1].split("@", 2); | |
| imp.name = seg0 + "/" + pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[2]); | |
| subPath = segs.slice(hasTargetSegment ? 3 : 2).join("/"); | |
| } else { | |
| [pkgNameNoScope, pkgVersion] = seg0.split("@", 2); | |
| imp.name = pkgNameNoScope; | |
| imp.version = pkgVersion; | |
| hasTargetSegment = ESM_TARGETS.has(segs[1]); | |
| subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/"); | |
| } |
No description provided.