Skip to content

Refactor add.ts#3

Merged
ije merged 6 commits intomainfrom
refactor
Mar 1, 2026
Merged

Refactor add.ts#3
ije merged 6 commits intomainfrom
refactor

Conversation

@ije
Copy link
Member

@ije ije commented Mar 1, 2026

No description provided.

Copilot AI review requested due to automatic review settings March 1, 2026 11:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/ImportMeta fields optional and updates dependency iteration to handle missing imports/peerImports.
  • Refactors fetch/meta caching and introduces META_CACHE_MEMO plus a module-level overridable fetcher via setFetcher.
  • 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
Comment on lines 258 to 261
} else if (source.startsWith("pr:")) {
imp.pr = true;
source = source.slice(3);
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +339 to +343
[pkgNameNoScope, pkgVersion] = seg0.split("@", 2);
imp.name = pkgNameNoScope;
imp.version = pkgVersion;
hasTargetSegment = ESM_TARGETS.has(segs[1]);
subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/");
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
[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("/");
}

Copilot uses AI. Check for mistakes.
Comment on lines +273 to +281
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;
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +333 to +339
[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);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[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] ?? "";

Copilot uses AI. Check for mistakes.
Comment on lines +339 to +343
[pkgNameNoScope, pkgVersion] = seg0.split("@", 2);
imp.name = pkgNameNoScope;
imp.version = pkgVersion;
hasTargetSegment = ESM_TARGETS.has(segs[1]);
subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/");
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
ije and others added 2 commits March 1, 2026 19:22
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +239 to +245
if (imp.github) {
prefix = "github:";
} else if (imp.jsr) {
prefix = "jsr:";
} else if (imp.pr) {
prefix = "pr:";
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 451 to 455
github: imp.github,
jsr: imp.jsr,
pr: imp.pr,
external: imp.external,
dev: imp.dev,
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +349 to +353
[pkgNameNoScope, pkgVersion] = seg0.split("@", 2);
imp.name = pkgNameNoScope;
imp.version = pkgVersion;
hasTargetSegment = ESM_TARGETS.has(segs[1]);
subPath = segs.slice(hasTargetSegment ? 2 : 1).join("/");
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[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("/");
}

Copilot uses AI. Check for mistakes.
@ije ije merged commit 7053781 into main Mar 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants