UNPKG

@jsenv/core

Version:

Tool to develop, test and build js projects

353 lines (340 loc) • 11.3 kB
/* * - should I restore eventual search params lost during node esm resolution * - what about symlinks? * It feels like I should apply symlink (when we don't want to preserve them) * once a file:/// url is found, regardless * if that comes from node resolution or anything else (not even magic resolution) * it should likely be an other plugin happening after the others */ import { applyNodeEsmResolution, defaultLookupPackageScope, defaultReadPackageJson, readCustomConditionsFromProcessArgs, } from "@jsenv/node-esm-resolution"; import { URL_META } from "@jsenv/url-meta"; import { urlToBasename, urlToExtension } from "@jsenv/urls"; import { readFileSync } from "node:fs"; export const createNodeEsmResolver = ({ runtimeCompat, rootDirectoryUrl, packageConditions = {}, preservesSymlink, }) => { const buildPackageConditions = createBuildPackageConditions( packageConditions, { rootDirectoryUrl, runtimeCompat, }, ); return (reference) => { if (reference.type === "package_json") { return reference.specifier; } const { ownerUrlInfo } = reference; if (reference.specifierPathname[0] === "/") { return null; // let it to jsenv_web_resolution } let parentUrl; if (reference.baseUrl) { parentUrl = reference.baseUrl; } else if (ownerUrlInfo.originalUrl?.startsWith("http")) { parentUrl = ownerUrlInfo.originalUrl; } else { parentUrl = ownerUrlInfo.url; } if (!parentUrl.startsWith("file:")) { return null; // let it to jsenv_web_resolution } const { specifier } = reference; // specifiers like "#something" have a special meaning for Node.js // but can also be used in .css and .html files for example and should not be modified // by node esm resolution const webResolutionFallback = ownerUrlInfo.type !== "js_module" || reference.type === "sourcemap_comment"; const conditions = buildPackageConditions(specifier, parentUrl, { webResolutionFallback, }); let resolution; const nodeEsmResolutionParams = { conditions, parentUrl, specifier, preservesSymlink, }; if (webResolutionFallback) { try { resolution = applyNodeEsmResolution(nodeEsmResolutionParams); } catch { return null; // delegate to web_resolution plugin } } else { resolution = applyNodeEsmResolution(nodeEsmResolutionParams); } const { url, type, isMain, packageDirectoryUrl } = resolution; // try to give a more meaningful filename after build if (isMain && packageDirectoryUrl) { const basename = urlToBasename(url); if (basename === "main" || basename === "index") { const parentBasename = urlToBasename(new URL("../../", url)); const dirname = urlToBasename(packageDirectoryUrl); let filenameHint = ""; if (parentBasename[0] === "@") { filenameHint += `${parentBasename}_`; } const extension = urlToExtension(url); filenameHint += `${dirname}_${basename}${extension}`; reference.filenameHint = filenameHint; } } if (ownerUrlInfo.context.build) { return url; } const dependsOnPackageJson = type !== "relative_specifier" && type !== "absolute_specifier" && type !== "node_builtin_specifier"; if (dependsOnPackageJson) { // this reference depends on package.json and node_modules // to be resolved. Each file using this specifier // must be invalidated when corresponding package.json changes addRelationshipWithPackageJson({ reference, packageJsonUrl: `${packageDirectoryUrl}package.json`, field: type.startsWith("field:") ? `#${type.slice("field:".length)}` : "", }); } // without this check a file inside a project without package.json // could be considered as a node module if there is a ancestor package.json // but we want to version only node modules if (url.includes("/node_modules/")) { const packageDirectoryUrl = defaultLookupPackageScope(url); if ( packageDirectoryUrl && packageDirectoryUrl !== ownerUrlInfo.context.rootDirectoryUrl ) { const packageVersion = defaultReadPackageJson(packageDirectoryUrl).version; // package version can be null, see https://github.com/babel/babel/blob/2ce56e832c2dd7a7ed92c89028ba929f874c2f5c/packages/babel-runtime/helpers/esm/package.json#L2 if (packageVersion) { addRelationshipWithPackageJson({ reference, packageJsonUrl: `${packageDirectoryUrl}package.json`, field: "version", hasVersioningEffect: true, }); } reference.version = packageVersion; } } return url; }; }; const createBuildPackageConditions = ( packageConditions, { rootDirectoryUrl, runtimeCompat }, ) => { const nodeRuntimeEnabled = Object.keys(runtimeCompat).includes("node"); // https://nodejs.org/api/esm.html#resolver-algorithm-specification const processArgConditions = readCustomConditionsFromProcessArgs(); const devResolver = (specifier, importer, { webResolutionFallback }) => { if (isBareSpecifier(specifier)) { let url; if (webResolutionFallback) { try { const resolution = applyNodeEsmResolution({ specifier, parentUrl: importer, }); url = resolution.url; } catch { url = new URL(specifier, importer).href; } } else { const resolution = applyNodeEsmResolution({ specifier, parentUrl: importer, }); url = resolution.url; } return !url.includes("/node_modules/"); } return !importer.includes("/node_modules/"); }; const conditionDefaultResolvers = { "dev:*": devResolver, "development": devResolver, "node": nodeRuntimeEnabled, "browser": !nodeRuntimeEnabled, "import": true, }; const conditionResolvers = { ...conditionDefaultResolvers, }; let wildcardToRemoveSet = new Set(); const addCustomResolver = (condition, customResolver) => { for (const conditionCandidate of Object.keys(conditionDefaultResolvers)) { if (conditionCandidate.includes("*")) { const conditionRegex = new RegExp( `^${conditionCandidate.replace(/\*/g, "(.*)")}$`, ); if (conditionRegex.test(condition)) { const existingResolver = conditionDefaultResolvers[conditionCandidate]; wildcardToRemoveSet.add(conditionCandidate); conditionResolvers[condition] = combineTwoPackageConditionResolvers( existingResolver, customResolver, ); return; } } } const existingResolver = conditionDefaultResolvers[condition]; if (existingResolver) { conditionResolvers[condition] = combineTwoPackageConditionResolvers( existingResolver, customResolver, ); return; } conditionResolvers[condition] = customResolver; }; for (const processArgCondition of processArgConditions) { addCustomResolver(processArgCondition, true); } for (const customCondition of Object.keys(packageConditions)) { const value = packageConditions[customCondition]; let customResolver; if (typeof value === "object") { const associations = URL_META.resolveAssociations( { applies: value }, (pattern) => { if (isBareSpecifier(pattern)) { try { if (pattern.endsWith("/")) { // avoid package path not exported const { packageDirectoryUrl } = applyNodeEsmResolution({ specifier: pattern.slice(0, -1), parentUrl: rootDirectoryUrl, }); return packageDirectoryUrl; } const { url } = applyNodeEsmResolution({ specifier: pattern, parentUrl: rootDirectoryUrl, }); return url; } catch { return new URL(pattern, rootDirectoryUrl); } } return new URL(pattern, rootDirectoryUrl); }, ); customResolver = (specifier, importer) => { if (isBareSpecifier(specifier)) { const { url } = applyNodeEsmResolution({ specifier, parentUrl: importer, }); const { applies } = URL_META.applyAssociations({ url, associations }); return applies; } return URL_META.applyAssociations({ url: importer, associations }) .applies; }; } else if (typeof value === "function") { customResolver = value; } else { customResolver = value; } addCustomResolver(customCondition, customResolver); } for (const wildcardToRemove of wildcardToRemoveSet) { delete conditionResolvers[wildcardToRemove]; } const conditionCandidateArray = Object.keys(conditionResolvers); return (specifier, importer, params) => { const conditions = []; for (const conditionCandidate of conditionCandidateArray) { const conditionResolver = conditionResolvers[conditionCandidate]; if (typeof conditionResolver === "function") { if (conditionResolver(specifier, importer, params)) { conditions.push(conditionCandidate); } } else if (conditionResolver) { conditions.push(conditionCandidate); } } return conditions; }; }; const combineTwoPackageConditionResolvers = (first, second) => { if (typeof second !== "function") { return second; } return (...args) => { const secondResult = second(...args); if (secondResult !== undefined) { return secondResult; } if (typeof first === "function") { return first(...args); } return first; }; }; const addRelationshipWithPackageJson = ({ reference, packageJsonUrl, field, hasVersioningEffect = false, }) => { const { ownerUrlInfo } = reference; for (const referenceToOther of ownerUrlInfo.referenceToOthersSet) { if ( referenceToOther.type === "package_json" && referenceToOther.subtype === field ) { return; } } const packageJsonReference = reference.addImplicit({ type: "package_json", subtype: field, specifier: packageJsonUrl, hasVersioningEffect, isWeak: true, }); // we don't cook package.json files, we just maintain their content // to be able to check if it has changed later on if (packageJsonReference.urlInfo.content === undefined) { const packageJsonContentAsBuffer = readFileSync(new URL(packageJsonUrl)); packageJsonReference.urlInfo.type = "json"; packageJsonReference.urlInfo.kitchen.urlInfoTransformer.setContent( packageJsonReference.urlInfo, String(packageJsonContentAsBuffer), ); } }; const isBareSpecifier = (specifier) => { if ( specifier[0] === "/" || specifier.startsWith("./") || specifier.startsWith("../") ) { return false; } try { // eslint-disable-next-line no-new new URL(specifier); return false; } catch { return true; } };