UNPKG

esbuild-plugin-cdn-imports

Version:

A esbuild plugin that resolves imports directly to a CDN.

426 lines (423 loc) 17.1 kB
import { builtinModules } from 'node:module'; import { join, extname } from 'node:path'; import { normalize } from 'node:path/posix'; import { parsePackage, normalizeCdnUrl } from 'cdn-resolve'; import { resolve, legacy } from '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 = normalize(path); const normalizedPath = normalized.replace(/\\+/g, "/").replace(/\/{2,}/g, "/"); const withLeadingSlash = normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`; return 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 || [], ...builtinModules, ...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 = 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 = 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(pkg, ".", { require: args.kind === "require-call" || args.kind === "require-resolve" }) || 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(pkg, subpathWithoutLeadingSlash, { require: args.kind === "require-call" || args.kind === "require-resolve" }) || 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 = join(basePath, dirPath, relativePath); debugLog("Resolved relative path:", resolvedPath); if (isCJS && !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 = 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(pkg, ".", { require: isCJS }) || 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(pkg, subpathWithoutLeadingSlash, { require: isCJS }) || 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 = 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 } }; } ); } }; } export { CDNImports };