Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8738023
fix(transforms): keep the subpath of scoped esm.sh specifiers
kojiwakayama Aug 25, 2026
63ca74f
fix(transforms): insert esm.sh subpaths ahead of a mapping's query
kojiwakayama Aug 25, 2026
87ce4b6
fix(transforms): resolve esm.sh specifiers through the canonical parser
kojiwakayama Aug 25, 2026
026452d
fix(transforms): treat remote TypeScript mappings as single modules
kojiwakayama Aug 25, 2026
661f49c
fix(transforms): keep trailing-slash esm.sh URLs resolvable, and subp…
kojiwakayama Aug 25, 2026
3052841
fix(transforms): keep a trailing separator that belongs to the subpath
kojiwakayama Aug 25, 2026
1cb0f49
fix(transforms): keep a package named like a build prefix resolvable
kojiwakayama Aug 25, 2026
16f1840
fix(transforms): resolve esm.sh import-map lookups in one place
kojiwakayama Aug 25, 2026
f48a01d
fix(transforms): recover packages named like reserved esm.sh segments
kojiwakayama Aug 25, 2026
c4d998f
fix(transforms): keep a package-root trailing separator, and use the …
kojiwakayama Aug 25, 2026
9f2c35d
fix(transforms): handle the stable channel and export-selecting mappings
kojiwakayama Aug 25, 2026
4db8475
fix(transforms): read a lone stable channel segment as a package root
kojiwakayama Aug 25, 2026
7660d17
fix(transforms): classify esm.sh export mappings, wasm, css, and chan…
kojiwakayama Aug 25, 2026
e338152
fix(transforms): keep reserved-name esm.sh mappings exact
kojiwakayama Aug 25, 2026
92486a1
fix(transforms): decide remote files by name shape, not an extension …
kojiwakayama Aug 25, 2026
de1358d
fix(transforms): recognise package-coordinate URLs before the filenam…
kojiwakayama Aug 25, 2026
68644cb
Preserve scoped esm.sh precedence across exact mappings
kojiwakayama Aug 25, 2026
07a09ac
refactor(transforms): split the per-table lookup out of the resolver
kojiwakayama Aug 25, 2026
5f5bd68
fix(transforms): recognise package roots served from a CDN path root
kojiwakayama Aug 25, 2026
0b95651
Keep esm.sh resolver coverage in one owner
kojiwakayama Aug 25, 2026
039ade0
fix(transforms): classify remote mappings by coordinate position
kojiwakayama Aug 25, 2026
d631f0e
fix(transforms): tie package routes to their CDNs and stamp-aware ext…
kojiwakayama Aug 25, 2026
9f143c6
fix(transforms): read a trailing separator on npm and jsr mappings as…
kojiwakayama Aug 25, 2026
5517aa7
fix(transforms): accept encoded scopes and build-prefixed reserved names
kojiwakayama Aug 25, 2026
96b9176
fix(transforms): normalise the specifier before reading its trailing …
kojiwakayama Aug 25, 2026
4b05b41
fix(transforms): recover reserved-name packages behind a build channel
kojiwakayama Aug 25, 2026
bbb9235
fix(transforms): classify encoded scoped CDN paths
kwakayama Aug 25, 2026
c5ba873
fix(transforms): normalize import map URL classification
kwakayama Aug 25, 2026
6b52056
fix(transforms): recognize equivalent CDN coordinates
kwakayama Aug 25, 2026
25aa33e
fix(transforms): normalize esm.sh mapping origins
kwakayama Aug 25, 2026
cb957b1
fix(transforms): tighten remote coordinate detection
kwakayama Aug 25, 2026
bda378b
fix(transforms): preserve esm.sh empty subpath segments
kwakayama Aug 25, 2026
5ecc1dc
fix(transforms): preserve numeric-leading file extensions
kwakayama Aug 25, 2026
dbf9e71
fix(transforms): recognize wildcard version mappings
kwakayama Aug 26, 2026
e297257
fix(transforms): classify encoded esm.sh routes
kwakayama Aug 26, 2026
5560361
fix(transforms): preserve range and export mappings
kwakayama Aug 26, 2026
65c4a3e
fix(transforms): classify encoded ranges and routes
kwakayama Aug 26, 2026
8a33fd8
fix(transforms): recognize dotted npm dist-tags
kwakayama Aug 26, 2026
9255738
fix(transforms): classify comparator wildcard ranges
kwakayama Aug 26, 2026
7056fdb
fix compound semver range classification
kojiwakayama Aug 26, 2026
52dedff
fix(transforms): classify compound semver ranges
kwakayama Aug 26, 2026
16a976d
fix(import-map): normalize source mappings
kwakayama Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/modules/import-map/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ describe("modules/import-map/resolver", () => {
);
});

it("should preserve repeated separators when selecting an esm.sh subpath", () => {
const map = { imports: { "pkg//sub": "/local.js" } };
assertEquals(
resolveImport("https://esm.sh/pkg@1//sub", map),
"/local.js",
"the dev-server resolver must select the exact empty-segment subpath entry",
);
});

it("should not append the esm.sh subpath to a local file mapping", () => {
const map = { imports: { react: "/local/react.ts" } };
assertEquals(
Expand All @@ -55,6 +64,14 @@ describe("modules/import-map/resolver", () => {
);
});

it("should not append the esm.sh subpath to a numeric-leading remote file mapping", () => {
const map = { imports: { "@scope/pkg": "https://cdn.example/archive.7z" } };
assertEquals(
resolveImport("https://esm.sh/@scope/pkg@1/sub", map),
"https://cdn.example/archive.7z",
);
});

it("should append the esm.sh subpath to an http mapping", () => {
const map = { imports: { react: "https://esm.sh/react@19" } };
assertEquals(
Expand All @@ -64,6 +81,38 @@ describe("modules/import-map/resolver", () => {
);
});

it("should normalize a URL-equivalent backslash before appending a subpath", () => {
const map = { imports: { pkg: "https://cdn.example/pkg\\" } };
assertEquals(
resolveImport("https://esm.sh/pkg@1/sub", map),
"https://cdn.example/pkg/sub",
);
});

it("should append through an encoded esm.sh GitHub source coordinate", () => {
const map = { imports: { pkg: "https://esm.sh/%67h/owner/repo" } };
assertEquals(
resolveImport("https://esm.sh/pkg@1/sub", map),
"https://esm.sh/%67h/owner/repo/sub",
);
});

it("should append the esm.sh subpath to a wildcard-version mapping", () => {
const map = { imports: { pkg: "https://cdn.example/pkg@1.x" } };
assertEquals(
resolveImport("https://esm.sh/pkg@1/sub", map),
"https://cdn.example/pkg@1.x/sub",
);
});

it("should append the esm.sh subpath to a compound-version mapping", () => {
const mapping = "https://cdn.example/pkg@1.2.3%20-%202.0.0-alpha.beta";
assertEquals(
resolveImport("https://esm.sh/pkg@1/sub", { imports: { pkg: mapping } }),
`${mapping}/sub`,
);
});

it("should resolve prefix mappings with trailing slash", () => {
const map = { imports: { "@lib/": "/src/lib/" } };
assertEquals(resolveImport("@lib/utils.ts", map), "/src/lib/utils.ts");
Expand All @@ -78,5 +127,17 @@ describe("modules/import-map/resolver", () => {
const map = { imports: { mylib: "/local/mylib.ts" } };
assertEquals(resolveImport("mylib.mjs", map), "/local/mylib.ts");
});

// This resolver backs the dev server's esbuild plugin, so the scoped
// subpath defect reported in #4098 reached production through here too,
// not only through the unified rewriter.
it("keeps the subpath of a scoped esm.sh specifier", () => {
const map = { imports: { "@scope/pkg": "https://cdn.example/pkg" } };
assertEquals(
resolveImport("https://esm.sh/@scope/pkg@1/sub", map),
"https://cdn.example/pkg/sub",
"a scoped subpath must reach its own entry point here as well",
);
});
});
});
90 changes: 14 additions & 76 deletions src/modules/import-map/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,5 @@
import type { ImportMapConfig } from "./types.ts";

/** Check if URL is an esm.sh URL */
function isEsmShUrl(url: string): boolean {
return url.startsWith("https://esm.sh/") || url.startsWith("http://esm.sh/");
}

function extractEsmShPackage(url: string): string | null {
if (!isEsmShUrl(url)) return null;

try {
const parsed = new URL(url);
const pathname = parsed.pathname.slice(1).replace(/^v\d+\//, "");

if (pathname.startsWith("@")) {
const packageName = pathname
.split("/")
.slice(0, 2)
.join("/")
.replace(/@[\d.]+.*$/, "");
return packageName || null;
}

const packageName = pathname.split("@")[0]?.split("/")[0] ?? "";
return packageName || null;
} catch (_) {
/* expected: URL may be malformed */
return null;
}
}

function extractEsmShSubpath(url: string): string {
const parsed = new URL(url);
const pathname = parsed.pathname.slice(1).replace(/^v\d+\//, "");

if (pathname.startsWith("@")) {
const parts = pathname.split("/");
if (parts.length <= 2) return "";

const packageParts = parts.slice(0, 2).join("/");
const afterPackage = pathname.slice(packageParts.length);
const versionMatch = afterPackage.match(/^@[^/]+(.*)$/);

return versionMatch?.[1] ?? "";
}

const firstSlash = pathname.indexOf("/");
if (firstSlash === -1) return "";

const restPath = pathname.slice(firstSlash);
return restPath.startsWith("/") ? restPath : "";
}
import { resolveEsmShThroughImportMap } from "#veryfront/transforms/shared/esm-sh-import-map.ts";

export function resolveImport(
specifier: string,
Expand All @@ -61,34 +11,22 @@ export function resolveImport(
const scopedExact = scopedImports?.[specifier];
if (scopedExact) return scopedExact;

const esmShMapping = resolveEsmShThroughImportMap(
specifier,
scopedImports,
undefined,
);
if (esmShMapping) return esmShMapping;

const globalExact = importMap.imports?.[specifier];
if (globalExact) return globalExact;

if (isEsmShUrl(specifier)) {
const esmShPackage = extractEsmShPackage(specifier);
if (esmShPackage) {
const subpath = extractEsmShSubpath(specifier);

// Always check for explicit subpath mapping first (e.g., "react/jsx-runtime")
// This takes priority over appending subpath to base package mapping
if (subpath) {
const fullKey = esmShPackage + subpath;
const subpathMapping = scopedImports?.[fullKey] ?? importMap.imports?.[fullKey];
if (subpathMapping) return subpathMapping;
}

const mapping = scopedImports?.[esmShPackage] ?? importMap.imports?.[esmShPackage];
if (mapping) {
if (!subpath) return mapping;

const isFilePath = !mapping.startsWith("http://") && !mapping.startsWith("https://") &&
!mapping.startsWith("npm:");
if (isFilePath) return mapping;

return mapping + subpath;
}
}
}
const globalEsmShMapping = resolveEsmShThroughImportMap(
specifier,
undefined,
importMap.imports,
);
if (globalEsmShMapping) return globalEsmShMapping;

if (specifier.endsWith(".js") || specifier.endsWith(".mjs") || specifier.endsWith(".cjs")) {
const base = specifier.replace(/\.(m|c)?js$/, "");
Expand Down
Loading
Loading