esbuild-plugin-cdn-imports
Version:
A esbuild plugin that resolves imports directly to a CDN.
428 lines (424 loc) • 17.3 kB
JavaScript
;
var module$1 = require('module');
var path = require('path');
var posix = require('path/posix');
var cdnResolve = require('cdn-resolve');
var resolve_exports = require('resolve.exports');
// src/index.ts
function resolveOptions(options) {
return {
cdn: options?.cdn ?? "esm",
exclude: options?.exclude || [],
versions: options?.versions || {},
defaultLoader: options?.defaultLoader || "js",
relativeImportsHandler: options?.relativeImportsHandler,
useJsdelivrEsm: options?.useJsdelivrEsm == null ? true : options.useJsdelivrEsm,
debug: options?.debug || false
};
}
var RESOLVE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".json"];
var CJS_EXTENSIONS = [".js", ".json", ".node", ".cjs"];
function isExternal(module, external) {
if (!Array.isArray(external)) {
throw new TypeError("external must be an array");
}
return external.find((it) => it === module || module.startsWith(`${it}/`));
}
var URL_RE = /^https?:\/\//;
var NPM_PATH_RE = /^\/npm\//;
function normalizeJsdelivrPath(pathname) {
return pathname.replace(NPM_PATH_RE, "/");
}
function normalizePath(cdn, path) {
const normalized = posix.normalize(path);
const normalizedPath = normalized.replace(/\\+/g, "/").replace(/\/{2,}/g, "/");
const withLeadingSlash = normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`;
return cdnResolve.normalizeCdnUrl(cdn, withLeadingSlash);
}
async function tryFileWithExtensions(cdnType, basePath, extensions) {
for (const ext of extensions) {
const url = `${basePath}${ext}`;
try {
const response = await fetch(normalizePath(cdnType, url), { method: "HEAD" });
if (response.ok) {
return url;
}
} catch {
}
}
return null;
}
async function tryIndexWithExtensions(cdnType, basePath, extensions) {
for (const ext of extensions) {
const indexPath = `${basePath}/index${ext}`;
try {
const response = await fetch(normalizePath(cdnType, indexPath), { method: "HEAD" });
if (response.ok) {
return indexPath;
}
} catch {
}
}
return null;
}
function CDNImports(options) {
const resolvedOptions = resolveOptions(options);
const debugLog = (...args) => {
if (resolvedOptions.debug) {
console.debug("[CDNImports]", ...args);
}
};
return {
name: "esbuild-cdn-imports",
setup(build) {
const externals = [
...build.initialOptions.external || [],
...module$1.builtinModules,
...module$1.builtinModules.map((it) => `node:${it}`)
];
const packageJsonCache = /* @__PURE__ */ new Map();
async function getPackageJson(packageName, version) {
const cacheKey = `${packageName}@${version}`;
if (packageJsonCache.has(cacheKey)) {
return packageJsonCache.get(cacheKey);
}
const url = normalizePath(
resolvedOptions.cdn,
`${packageName}@${version}/package.json`
);
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch package.json for ${packageName}@${version}: ${res.status}`);
}
const pkg = await res.json();
packageJsonCache.set(cacheKey, pkg);
return pkg;
}
build.onResolve({ filter: URL_RE }, (args) => ({
path: args.path,
namespace: "cdn-imports"
}));
build.onResolve({ filter: /.*/, namespace: "cdn-imports" }, async (args) => {
debugLog("Intercepted import from CDN:", args);
if (isExternal(args.path, externals)) {
return {
external: true,
path: args.path
};
}
if (!args.path.startsWith(".")) {
let path = args.path;
if (args.path.startsWith("/npm/") && resolvedOptions.cdn === "jsdelivr") {
path = normalizeJsdelivrPath(args.path);
debugLog("Normalized /npm/ path:", { original: args.path, normalized: path });
}
const isPackageWithSubpath = path.includes("/") && !path.startsWith("http");
if (isPackageWithSubpath) {
try {
const url2 = normalizePath(resolvedOptions.cdn, path);
const response = await fetch(url2, { method: "HEAD" });
if (response.ok) {
debugLog("Package with subpath exists, using directly:", url2);
return {
path: url2,
namespace: "cdn-imports",
pluginData: {
jsdelivrEsm: resolvedOptions.useJsdelivrEsm && resolvedOptions.cdn === "jsdelivr"
}
};
}
const parsed2 = cdnResolve.parsePackage(path);
if (resolvedOptions.versions[parsed2.name]) {
parsed2.version = resolvedOptions.versions[parsed2.name];
}
const packageUrl = normalizePath(
resolvedOptions.cdn,
`${parsed2.name}@${parsed2.version}${parsed2.path || ""}`
);
const packageResponse = await fetch(packageUrl, { method: "HEAD" });
if (packageResponse.ok) {
debugLog("Package with subpath resolved, using:", packageUrl);
return {
path: packageUrl,
namespace: "cdn-imports",
pluginData: {
jsdelivrEsm: resolvedOptions.useJsdelivrEsm && resolvedOptions.cdn === "jsdelivr"
}
};
}
} catch (e) {
debugLog("Failed to resolve package with subpath:", e);
}
} else {
try {
const url2 = normalizePath(resolvedOptions.cdn, path);
const response = await fetch(url2, { method: "HEAD" });
debugLog("Direct URL check:", { url: url2, redirected: response.redirected });
if (response.ok && !response.redirected) {
debugLog("URL exists, using directly:", url2);
return {
path: url2,
namespace: "cdn-imports",
pluginData: {
jsdelivrEsm: resolvedOptions.useJsdelivrEsm && resolvedOptions.cdn === "jsdelivr"
}
};
}
} catch (e) {
debugLog("Failed to fetch URL directly:", e);
}
}
const parsed = cdnResolve.parsePackage(path);
if (resolvedOptions.versions[parsed.name]) {
parsed.version = resolvedOptions.versions[parsed.name];
}
let subpath = parsed.path;
try {
const pkg = await getPackageJson(parsed.name, parsed.version);
if (!subpath) {
const resolvedExport = resolve_exports.resolve(pkg, ".", {
require: args.kind === "require-call" || args.kind === "require-resolve"
}) || resolve_exports.legacy(pkg);
if (typeof resolvedExport === "string") {
subpath = resolvedExport.replace(/^\.?\/?/, "/");
} else if (Array.isArray(resolvedExport) && resolvedExport.length > 0) {
subpath = resolvedExport[0].replace(/^\.?\/?/, "/");
}
} else {
const subpathWithoutLeadingSlash = subpath.startsWith("/") ? subpath.slice(1) : subpath;
const resolvedExport = resolve_exports.resolve(pkg, subpathWithoutLeadingSlash, {
require: args.kind === "require-call" || args.kind === "require-resolve"
}) || resolve_exports.legacy(pkg);
if (typeof resolvedExport === "string") {
subpath = resolvedExport.replace(/^\.?\/?/, "/");
} else if (Array.isArray(resolvedExport) && resolvedExport.length > 0) {
subpath = resolvedExport[0].replace(/^\.?\/?/, "/");
}
debugLog("Resolved subpath:", {
name: parsed.name,
version: parsed.version,
subpath,
resolvedExport
});
}
if (subpath && subpath[0] !== "/") {
subpath = `/${subpath}`;
}
debugLog("Resolved main module path:", {
name: parsed.name,
version: parsed.version,
subpath
});
return {
path: normalizePath(
resolvedOptions.cdn,
`${parsed.name}@${parsed.version}${subpath == null ? "" : subpath}`
),
pluginData: {
jsdelivrEsm: resolvedOptions.useJsdelivrEsm && resolvedOptions.cdn === "jsdelivr"
},
namespace: "cdn-imports"
};
} catch (e) {
debugLog("Error resolving package:", e);
return {
path: normalizePath(resolvedOptions.cdn, path),
namespace: "cdn-imports"
};
}
}
const url = new URL(args.pluginData.packageUrl);
debugLog("Resolving relative import:", { url, path: args.path });
const isCJS = args.kind === "require-call" || args.kind === "require-resolve";
let urlPathname = url.pathname;
if (resolvedOptions.cdn === "jsdelivr" && NPM_PATH_RE.test(urlPathname)) {
urlPathname = normalizeJsdelivrPath(urlPathname);
debugLog("Normalized URL pathname:", { original: url.pathname, normalized: urlPathname });
}
const relativePath = args.path;
if (relativePath.startsWith("./") || relativePath.startsWith("../")) {
const segments = urlPathname.split("/");
const versionEndIndex = segments.findIndex((s) => s.includes("@") && !s.startsWith("@"));
if (versionEndIndex !== -1) {
const basePath = segments.slice(0, versionEndIndex + 1).join("/");
const dirPath = segments.slice(versionEndIndex + 1, -1).join("/");
const resolvedPath = path.join(basePath, dirPath, relativePath);
debugLog("Resolved relative path:", resolvedPath);
if (isCJS && !path.extname(resolvedPath)) {
const resolvedWithExt = await tryFileWithExtensions(
resolvedOptions.cdn,
resolvedPath,
CJS_EXTENSIONS
);
if (resolvedWithExt) {
debugLog("Resolved with extension:", resolvedWithExt);
return {
path: normalizePath(resolvedOptions.cdn, resolvedWithExt),
namespace: "cdn-imports"
};
}
const resolvedIndex = await tryIndexWithExtensions(
resolvedOptions.cdn,
resolvedPath,
CJS_EXTENSIONS
);
if (resolvedIndex) {
debugLog("Resolved as directory index:", resolvedIndex);
return {
path: normalizePath(resolvedOptions.cdn, resolvedIndex),
namespace: "cdn-imports"
};
}
}
return {
path: normalizePath(resolvedOptions.cdn, resolvedPath),
namespace: "cdn-imports"
};
}
}
return {
path: normalizePath(resolvedOptions.cdn, urlPathname),
namespace: "cdn-imports"
};
});
build.onResolve({ filter: /.*/ }, async (args) => {
if (args.kind === "entry-point") {
return null;
}
if (args.path.startsWith(".")) {
if (resolvedOptions.relativeImportsHandler) {
return resolvedOptions.relativeImportsHandler(args, build);
}
return null;
}
if (args.path.startsWith("/npm/") && resolvedOptions.cdn === "jsdelivr") {
const normalizedPath = normalizeJsdelivrPath(args.path);
debugLog("Normalized /npm/ path:", { original: args.path, normalized: normalizedPath });
return {
path: normalizePath(resolvedOptions.cdn, normalizedPath),
namespace: "cdn-imports"
};
}
try {
const parsed = cdnResolve.parsePackage(args.path);
if (resolvedOptions.exclude.includes(parsed.name)) {
return null;
}
if (isExternal(args.path, externals)) {
return {
external: true,
path: args.path
};
}
if (resolvedOptions.versions[parsed.name]) {
parsed.version = resolvedOptions.versions[parsed.name];
}
let subpath = parsed.path;
const isCJS = args.kind === "require-call" || args.kind === "require-resolve";
try {
const pkg = await getPackageJson(parsed.name, parsed.version);
if (!subpath) {
const resolvedExport = resolve_exports.resolve(pkg, ".", { require: isCJS }) || resolve_exports.legacy(pkg);
if (typeof resolvedExport === "string") {
subpath = resolvedExport.replace(/^\.?\/?/, "/");
} else if (Array.isArray(resolvedExport) && resolvedExport.length > 0) {
subpath = resolvedExport[0].replace(/^\.?\/?/, "/");
} else if (isCJS && pkg.main) {
subpath = `/${pkg.main}`;
} else if (!isCJS && pkg.module) {
subpath = `/${pkg.module}`;
} else if (pkg.main) {
subpath = `/${pkg.main}`;
}
} else {
const subpathWithoutLeadingSlash = subpath.startsWith("/") ? subpath.slice(1) : subpath;
const resolvedExport = resolve_exports.resolve(pkg, subpathWithoutLeadingSlash, { require: isCJS }) || resolve_exports.legacy(pkg);
if (typeof resolvedExport === "string") {
subpath = resolvedExport.replace(/^\.?\/?/, "/");
} else if (Array.isArray(resolvedExport) && resolvedExport.length > 0) {
subpath = resolvedExport[0].replace(/^\.?\/?/, "/");
} else if (isCJS) {
const baseSubpath = subpath.startsWith("/") ? subpath : `/${subpath}`;
const resolvedWithExt = await tryFileWithExtensions(
resolvedOptions.cdn,
`${parsed.name}@${parsed.version}${baseSubpath}`,
CJS_EXTENSIONS
);
if (resolvedWithExt) {
return {
path: normalizePath(resolvedOptions.cdn, resolvedWithExt),
namespace: "cdn-imports"
};
}
}
}
if (subpath && subpath[0] !== "/") {
subpath = `/${subpath}`;
}
debugLog("Final resolved path:", {
name: parsed.name,
version: parsed.version,
subpath,
isCJS
});
return {
path: normalizePath(
resolvedOptions.cdn,
`${parsed.name}@${parsed.version}${subpath}`
),
pluginData: {
jsdelivrEsm: !isCJS && resolvedOptions.useJsdelivrEsm && resolvedOptions.cdn === "jsdelivr"
},
namespace: "cdn-imports"
};
} catch (e) {
debugLog("Error resolving package:", e);
const fullPath = parsed.path ? `${parsed.name}@${parsed.version}${parsed.path}` : `${parsed.name}@${parsed.version}`;
return {
path: normalizePath(resolvedOptions.cdn, fullPath),
namespace: "cdn-imports"
};
}
} catch (e) {
debugLog("Error parsing package:", e);
return null;
}
});
build.onLoad(
{
filter: /.*/,
namespace: "cdn-imports"
},
async (args) => {
debugLog("Loading:", args.path);
const hasJsdelivrEsm = args.pluginData != null && "jsdelivrEsm" in args.pluginData && args.pluginData.jsdelivrEsm;
if (resolvedOptions.useJsdelivrEsm && resolvedOptions.cdn === "jsdelivr" && hasJsdelivrEsm && !args.path.endsWith("+esm")) {
args.path += "/+esm";
}
const normalizedPath = args.path.replace(/(?<!:)\/{2,}/g, "/").replace(/\\+/g, "/");
debugLog("Load Normalized path:", { original: args.path, normalized: normalizedPath });
const res = await fetch(normalizedPath);
if (!res.ok) {
throw new Error(`failed to load ${res.url}: ${res.status}`);
}
let loader = resolvedOptions.defaultLoader;
const ext = path.extname(res.url);
if (RESOLVE_EXTENSIONS.includes(ext)) {
loader = ext.slice(1);
} else if (ext === ".mjs" || ext === ".cjs") {
loader = "js";
}
debugLog("Loaded file:", { url: res.url, loader });
return {
contents: new Uint8Array(await res.arrayBuffer()),
loader,
pluginData: {
packageUrl: res.url
}
};
}
);
}
};
}
exports.CDNImports = CDNImports;