UNPKG

@jsenv/node-module-import-map

Version:
1,832 lines (1,602 loc) 69.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var importMap = require('@jsenv/import-map'); var logger = require('@jsenv/logger'); var util = require('@jsenv/util'); var isSpecifierForNodeCoreModule_js = require('@jsenv/import-map/src/isSpecifierForNodeCoreModule.js'); var module$1 = require('module'); var cancellation = require('@jsenv/cancellation'); const memoizeAsyncFunctionByUrl = fn => { const cache = {}; return memoizeAsyncFunction(fn, { getMemoryEntryFromArguments: ([url]) => { return { get: () => { return cache[url]; }, set: promise => { cache[url] = promise; }, delete: () => { delete cache[url]; } }; } }); }; const memoizeAsyncFunctionBySpecifierAndImporter = fn => { const importerCache = {}; return memoizeAsyncFunction(fn, { getMemoryEntryFromArguments: ([specifier, importer]) => { return { get: () => { const specifierCacheForImporter = importerCache[importer]; return specifierCacheForImporter ? specifierCacheForImporter[specifier] : null; }, set: promise => { const specifierCacheForImporter = importerCache[importer]; if (specifierCacheForImporter) { specifierCacheForImporter[specifier] = promise; } else { importerCache[importer] = { [specifier]: promise }; } }, delete: () => { const specifierCacheForImporter = importerCache[importer]; if (specifierCacheForImporter) { delete specifierCacheForImporter[specifier]; } } }; } }); }; const memoizeAsyncFunction = (fn, { getMemoryEntryFromArguments }) => { const memoized = async (...args) => { const memoryEntry = getMemoryEntryFromArguments(args); const promiseFromMemory = memoryEntry.get(); if (promiseFromMemory) { return promiseFromMemory; } const { promise, resolve, reject } = createControllablePromise(); memoryEntry.set(promise); let value; let error; try { value = fn(...args); error = false; } catch (e) { value = e; error = true; memoryEntry.delete(); } if (error) { reject(error); } else { resolve(value); } return promise; }; memoized.isInMemory = (...args) => { return Boolean(getMemoryEntryFromArguments(args).get()); }; return memoized; }; const createControllablePromise = () => { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }; /* global __filename */ const filenameContainsBackSlashes = __filename.indexOf("\\") > -1; const url = filenameContainsBackSlashes ? `file:///${__filename.replace(/\\/g, "/")}` : `file://${__filename}`; const require$1 = module$1.createRequire(url); const parser = require$1("@babel/parser"); const traverse = require$1("@babel/traverse"); const parseSpecifiersFromFile = async (fileUrl, { fileContent, jsFilesParsingOptions } = {}) => { fileContent = fileContent === undefined ? await util.readFile(fileUrl, { as: "string" }) : fileContent; const fileExtension = util.urlToExtension(fileUrl); const { jsx = [".jsx", ".tsx"].includes(fileExtension), typescript = [".ts", ".tsx"].includes(util.urlToExtension(fileUrl)), flow = false } = jsFilesParsingOptions; const ast = parser.parse(fileContent, { sourceType: "module", sourceFilename: util.urlToFileSystemPath(fileUrl), plugins: ["topLevelAwait", "exportDefaultFrom", ...(jsx ? ["jsx"] : []), ...(typescript ? ["typescript"] : []), ...(flow ? ["flow"] : [])], ...jsFilesParsingOptions, ranges: true }); const specifiers = {}; const addSpecifier = ({ path, type }) => { const specifier = path.node.value; specifiers[specifier] = { line: path.node.loc.start.line, column: path.node.loc.start.column, type }; }; traverse.default(ast, { // "ImportExpression is replaced with a CallExpression whose callee is an Import node." // https://babeljs.io/docs/en/babel-parser#output // ImportExpression: (path) => { // if (path.node.arguments[0].type !== "StringLiteral") { // // Non-string argument, probably a variable or expression, e.g. // // import(moduleId) // // import('./' + moduleName) // return // } // addSpecifier(path.get("arguments")[0]) // }, CallExpression: path => { if (path.node.callee.type !== "Import") { // Some other function call, not import(); return; } if (path.node.arguments[0].type !== "StringLiteral") { // Non-string argument, probably a variable or expression, e.g. // import(moduleId) // import('./' + moduleName) return; } addSpecifier({ path: path.get("arguments")[0], type: "import-dynamic" }); }, ExportAllDeclaration: path => { addSpecifier({ path: path.get("source"), type: "export-all" }); }, ExportNamedDeclaration: path => { if (!path.node.source) { // This export has no "source", so it's probably // a local variable or function, e.g. // export { varName } // export const constName = ... // export function funcName() {} return; } addSpecifier({ path: path.get("source"), type: "export-named" }); }, ImportDeclaration: path => { addSpecifier({ path: path.get("source"), type: "import-static" }); } }); return specifiers; }; // https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/css-syntax-error.js#L43 // https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/terminal-highlight.js#L50 // https://github.com/babel/babel/blob/eea156b2cb8deecfcf82d52aa1b71ba4995c7d68/packages/babel-code-frame/src/index.js#L1 const showSource = ({ url, line, column, source }) => { let message = ""; message += typeof url === "undefined" ? "Anonymous" : url; if (typeof line !== "number") { return message; } message += `:${line}`; if (typeof column === "number") { message += `:${column}`; } if (!source) { return message; } return `${message} ${showSourceLocation(source, { line, column })}`; }; const red = "\x1b[31m"; const grey = "\x1b[39m"; const ansiResetSequence = "\x1b[0m"; const showSourceLocation = (source, { line, column, numberOfSurroundingLinesToShow = 1, lineMaxLength = 120, color = false, markColor = red, asideColor = grey, colorMark = string => `${markColor}${string}${ansiResetSequence}`, colorAside = string => `${asideColor}${string}${ansiResetSequence}` }) => { const mark = color ? colorMark : string => string; const aside = color ? colorAside : string => string; const lines = source.split(/\r?\n/); let lineRange = { start: line - 1, end: line }; lineRange = moveLineRangeUp(lineRange, numberOfSurroundingLinesToShow); lineRange = moveLineRangeDown(lineRange, numberOfSurroundingLinesToShow); lineRange = lineRangeWithinLines(lineRange, lines); const linesToShow = lines.slice(lineRange.start, lineRange.end); const endLineNumber = lineRange.end; const lineNumberMaxWidth = String(endLineNumber).length; const columnRange = {}; if (column === undefined) { columnRange.start = 0; columnRange.end = lineMaxLength; } else if (column > lineMaxLength) { columnRange.start = column - Math.floor(lineMaxLength / 2); columnRange.end = column + Math.ceil(lineMaxLength / 2); } else { columnRange.start = 0; columnRange.end = lineMaxLength; } return linesToShow.map((lineSource, index) => { const lineNumber = lineRange.start + index + 1; const isMainLine = lineNumber === line; const lineSourceTruncated = applyColumnRange(columnRange, lineSource); const lineNumberWidth = String(lineNumber).length; // ensure if line moves from 7,8,9 to 10 the display is still great const lineNumberRightSpacing = " ".repeat(lineNumberMaxWidth - lineNumberWidth); const asideSource = `${lineNumber}${lineNumberRightSpacing} |`; const lineFormatted = `${aside(asideSource)} ${lineSourceTruncated}`; if (isMainLine) { if (column === undefined) { return `${mark(">")} ${lineFormatted}`; } const lineSourceUntilColumn = lineSourceTruncated.slice(0, column - columnRange.start); const spacing = stringToSpaces(lineSourceUntilColumn); const mainLineFormatted = `${mark(">")} ${lineFormatted} ${" ".repeat(lineNumberWidth)} ${aside("|")}${spacing}${mark("^")}`; return mainLineFormatted; } return ` ${lineFormatted}`; }).join(` `); }; const applyColumnRange = ({ start, end }, line) => { if (typeof start !== "number") { throw new TypeError(`start must be a number, received ${start}`); } if (typeof end !== "number") { throw new TypeError(`end must be a number, received ${end}`); } if (end < start) { throw new Error(`end must be greater than start, but ${end} is smaller than ${start}`); } const prefix = "…"; const suffix = "…"; const lastIndex = line.length; if (line.length === 0) { // don't show any ellipsis if the line is empty // because it's not truncated in that case return ""; } const startTruncated = start > 0; const endTruncated = lastIndex > end; let from = startTruncated ? start + prefix.length : start; let to = endTruncated ? end - suffix.length : end; if (to > lastIndex) to = lastIndex; if (start >= lastIndex || from === to) { return ""; } let result = ""; while (from < to) { result += line[from]; from++; } if (result.length === 0) { return ""; } if (startTruncated && endTruncated) { return `${prefix}${result}${suffix}`; } if (startTruncated) { return `${prefix}${result}`; } if (endTruncated) { return `${result}${suffix}`; } return result; }; const stringToSpaces = string => string.replace(/[^\t]/g, " "); // const getLineRangeLength = ({ start, end }) => end - start const moveLineRangeUp = ({ start, end }, number) => { return { start: start - number, end }; }; const moveLineRangeDown = ({ start, end }, number) => { return { start, end: end + number }; }; const lineRangeWithinLines = ({ start, end }, lines) => { return { start: start < 0 ? 0 : start, end: end > lines.length ? lines.length : end }; }; const resolveFile = async (fileUrl, { magicExtensions }) => { const fileStat = await util.readFileSystemNodeStat(fileUrl, { nullIfNotFound: true }); // file found if (fileStat && fileStat.isFile()) { return fileUrl; } // directory found if (fileStat && fileStat.isDirectory()) { const indexFileSuffix = fileUrl.endsWith("/") ? "index" : "/index"; const indexFileUrl = `${fileUrl}${indexFileSuffix}`; const extensionLeadingToAFile = await findExtensionLeadingToFile(indexFileUrl, magicExtensions); if (extensionLeadingToAFile === null) { return null; } return `${indexFileUrl}${extensionLeadingToAFile}`; } // file not found and it has an extension const extension = util.urlToExtension(fileUrl); if (extension !== "") { return null; } const extensionLeadingToAFile = await findExtensionLeadingToFile(fileUrl, magicExtensions); // magic extension not found if (extensionLeadingToAFile === null) { return null; } // magic extension worked return `${fileUrl}${extensionLeadingToAFile}`; }; const findExtensionLeadingToFile = async (fileUrl, magicExtensions) => { const urlDirectoryUrl = util.resolveUrl("./", fileUrl); const urlFilename = util.urlToFilename(fileUrl); const extensionLeadingToFile = await cancellation.firstOperationMatching({ array: magicExtensions, start: async extensionCandidate => { const urlCandidate = `${urlDirectoryUrl}${urlFilename}${extensionCandidate}`; const stats = await util.readFileSystemNodeStat(urlCandidate, { nullIfNotFound: true }); return stats && stats.isFile() ? extensionCandidate : null; }, predicate: extension => Boolean(extension) }); return extensionLeadingToFile || null; }; const createPackageNameMustBeAStringWarning = ({ packageName, packageFileUrl }) => { return { code: "PACKAGE_NAME_MUST_BE_A_STRING", message: `package name field must be a string --- package name field --- ${packageName} --- package.json file path --- ${packageFileUrl}` }; }; const BARE_SPECIFIER_ERROR = {}; const getImportMapFromJsFiles = async ({ logger, warn, projectDirectoryUrl, importMap: importMap$1, magicExtensions, runtime, treeshakeMappings, jsFilesParsingOptions }) => { const projectPackageFileUrl = util.resolveUrl("./package.json", projectDirectoryUrl); const imports = {}; const scopes = {}; const addMapping = ({ scope, from, to }) => { if (scope) { scopes[scope] = { ...(scopes[scope] || {}), [from]: to }; } else { imports[from] = to; } }; const topLevelMappingsUsed = []; const scopedMappingsUsed = {}; const markMappingAsUsed = ({ scope, from, to }) => { if (scope) { if (scope in scopedMappingsUsed) { scopedMappingsUsed[scope].push({ from, to }); } else { scopedMappingsUsed[scope] = [{ from, to }]; } } else { topLevelMappingsUsed.push({ from, to }); } }; const importMapNormalized = importMap.normalizeImportMap(importMap$1, projectDirectoryUrl); const trackAndResolveImport = (specifier, importer) => { return importMap.resolveImport({ specifier, importer, importMap: importMapNormalized, defaultExtension: false, onImportMapping: ({ scope, from }) => { if (scope) { // make scope relative again scope = `./${util.urlToRelativeUrl(scope, projectDirectoryUrl)}`; // make from relative again if (from.startsWith(projectDirectoryUrl)) { from = `./${util.urlToRelativeUrl(from, projectDirectoryUrl)}`; } } markMappingAsUsed({ scope, from, to: scope ? importMap$1.scopes[scope][from] : importMap$1.imports[from] }); }, createBareSpecifierError: () => BARE_SPECIFIER_ERROR }); }; const resolveFileSystemUrl = memoizeAsyncFunctionBySpecifierAndImporter(async (specifier, importer, { importedBy }) => { if (runtime === "node" && isSpecifierForNodeCoreModule_js.isSpecifierForNodeCoreModule(specifier)) { return null; } let fileUrl; let gotBareSpecifierError = false; try { fileUrl = trackAndResolveImport(specifier, importer); } catch (e) { if (e !== BARE_SPECIFIER_ERROR) { throw e; } if (importer === projectPackageFileUrl) { // cannot find package main file (package.main is "" for instance) // we can't discover main file and parse dependencies return null; } gotBareSpecifierError = true; fileUrl = util.resolveUrl(specifier, importer); } const fileUrlOnFileSystem = await resolveFile(fileUrl, { magicExtensions: magicExtensionWithImporterExtension(magicExtensions, importer) }); if (!fileUrlOnFileSystem) { warn(createFileNotFoundWarning({ specifier, importedBy, fileUrl, magicExtensions })); return null; } const needsAutoMapping = fileUrlOnFileSystem !== fileUrl || gotBareSpecifierError; if (needsAutoMapping) { const packageDirectoryUrl = packageDirectoryUrlFromUrl(fileUrl, projectDirectoryUrl); const packageFileUrl = util.resolveUrl("package.json", packageDirectoryUrl); const autoMapping = { scope: packageFileUrl === projectPackageFileUrl ? undefined : `./${util.urlToRelativeUrl(packageDirectoryUrl, projectDirectoryUrl)}`, from: specifier, to: `./${util.urlToRelativeUrl(fileUrlOnFileSystem, projectDirectoryUrl)}` }; addMapping(autoMapping); markMappingAsUsed(autoMapping); const closestPackageObject = await util.readFile(packageFileUrl, { as: "json" }); // it's imprecise because we are not ensuring the wildcard correspond to automapping // but good enough for now const containsWildcard = Object.keys(closestPackageObject.exports || {}).some(key => key.includes("*")); const autoMappingWarning = formatAutoMappingSpecifierWarning({ specifier, importedBy, autoMapping, closestPackageDirectoryUrl: packageDirectoryUrl, closestPackageObject }); if (containsWildcard) { logger.debug(autoMappingWarning); } else { warn(autoMappingWarning); } } return fileUrlOnFileSystem; }); const visitFile = memoizeAsyncFunctionByUrl(async fileUrl => { const fileContent = await util.readFile(fileUrl, { as: "string" }); const specifiers = await parseSpecifiersFromFile(fileUrl, { fileContent, jsFilesParsingOptions }); const dependencies = await Promise.all(Object.keys(specifiers).map(async specifier => { const specifierInfo = specifiers[specifier]; const dependencyUrlOnFileSystem = await resolveFileSystemUrl(specifier, fileUrl, { importedBy: showSource({ url: fileUrl, line: specifierInfo.line, column: specifierInfo.column, source: fileContent }) }); return dependencyUrlOnFileSystem; })); const dependenciesToVisit = dependencies.filter(dependency => { return dependency && !visitFile.isInMemory(dependency); }); await Promise.all(dependenciesToVisit.map(dependency => { return visitFile(dependency); })); }); const projectPackageObject = await util.readFile(projectPackageFileUrl, { as: "json" }); const projectPackageName = projectPackageObject.name; if (typeof projectPackageName !== "string") { warn(createPackageNameMustBeAStringWarning({ packageName: projectPackageName, packageFileUrl: projectPackageFileUrl })); return importMap$1; } const projectMainFileUrlOnFileSystem = await resolveFileSystemUrl(projectPackageName, projectPackageFileUrl, { importedBy: projectPackageObject.exports ? `${projectPackageFileUrl}#exports` : `${projectPackageFileUrl}` }); if (projectMainFileUrlOnFileSystem) { await visitFile(projectMainFileUrlOnFileSystem); } if (treeshakeMappings) { const importsUsed = {}; topLevelMappingsUsed.forEach(({ from, to }) => { importsUsed[from] = to; }); const scopesUsed = {}; Object.keys(scopedMappingsUsed).forEach(scope => { const mappingsUsed = scopedMappingsUsed[scope]; const scopedMappings = {}; mappingsUsed.forEach(({ from, to }) => { scopedMappings[from] = to; }); scopesUsed[scope] = scopedMappings; }); return importMap.sortImportMap({ imports: importsUsed, scopes: scopesUsed }); } return importMap.sortImportMap(importMap.composeTwoImportMaps(importMap$1, { imports, scopes })); }; const packageDirectoryUrlFromUrl = (url, projectDirectoryUrl) => { const relativeUrl = util.urlToRelativeUrl(url, projectDirectoryUrl); const lastNodeModulesDirectoryStartIndex = relativeUrl.lastIndexOf("node_modules/"); if (lastNodeModulesDirectoryStartIndex === -1) { return projectDirectoryUrl; } const lastNodeModulesDirectoryEndIndex = lastNodeModulesDirectoryStartIndex + `node_modules/`.length; const beforeNodeModulesLastDirectory = relativeUrl.slice(0, lastNodeModulesDirectoryEndIndex); const afterLastNodeModulesDirectory = relativeUrl.slice(lastNodeModulesDirectoryEndIndex); const remainingDirectories = afterLastNodeModulesDirectory.split("/"); if (afterLastNodeModulesDirectory[0] === "@") { // scoped package return `${projectDirectoryUrl}${beforeNodeModulesLastDirectory}${remainingDirectories.slice(0, 2).join("/")}`; } return `${projectDirectoryUrl}${beforeNodeModulesLastDirectory}${remainingDirectories[0]}/`; }; const magicExtensionWithImporterExtension = (magicExtensions, importer) => { const importerExtension = util.urlToExtension(importer); const magicExtensionsWithoutImporterExtension = magicExtensions.filter(ext => ext !== importerExtension); return [importerExtension, ...magicExtensionsWithoutImporterExtension]; }; const createFileNotFoundWarning = ({ specifier, importedBy, fileUrl, magicExtensions }) => { return { code: "FILE_NOT_FOUND", message: logger.createDetailedMessage(`Cannot find file for "${specifier}"`, { "specifier origin": importedBy, "file url tried": fileUrl, ...(util.urlToExtension(fileUrl) === "" ? { ["extensions tried"]: magicExtensions.join(`, `) } : {}) }) }; }; const formatAutoMappingSpecifierWarning = ({ importedBy, autoMapping, closestPackageDirectoryUrl, closestPackageObject }) => { return { code: "AUTO_MAPPING", message: logger.createDetailedMessage(`Auto mapping ${autoMapping.from} to ${autoMapping.to}.`, { "specifier origin": importedBy, "suggestion": decideAutoMappingSuggestion({ autoMapping, closestPackageDirectoryUrl, closestPackageObject }) }) }; }; const decideAutoMappingSuggestion = ({ autoMapping, closestPackageDirectoryUrl, closestPackageObject }) => { if (typeof closestPackageObject.importmap === "string") { const packageImportmapFileUrl = util.resolveUrl(closestPackageObject.importmap, closestPackageDirectoryUrl); return `To get rid of this warning, add an explicit mapping into importmap file. ${mappingToImportmapString(autoMapping)} into ${packageImportmapFileUrl}.`; } return `To get rid of this warning, add an explicit mapping into package.json. ${mappingToExportsFieldString(autoMapping)} into ${closestPackageDirectoryUrl}package.json.`; }; const mappingToImportmapString = ({ scope, from, to }) => { if (scope) { return JSON.stringify({ scopes: { [scope]: { [from]: to } } }, null, " "); } return JSON.stringify({ imports: { [from]: to } }, null, " "); }; const mappingToExportsFieldString = ({ scope, from, to }) => { if (scope) { const scopeUrl = util.resolveUrl(scope, "file://"); const toUrl = util.resolveUrl(to, "file://"); to = `./${util.urlToRelativeUrl(toUrl, scopeUrl)}`; } return JSON.stringify({ exports: { [from]: to } }, null, " "); }; const optimizeImportMap = ({ imports, scopes }) => { // remove useless duplicates (scoped key+value already defined on imports) const scopesOptimized = {}; Object.keys(scopes).forEach(scope => { const scopeMappings = scopes[scope]; const scopeMappingsOptimized = {}; Object.keys(scopeMappings).forEach(mappingKey => { const topLevelMappingValue = imports[mappingKey]; const mappingValue = scopeMappings[mappingKey]; if (!topLevelMappingValue || topLevelMappingValue !== mappingValue) { scopeMappingsOptimized[mappingKey] = mappingValue; } }); if (Object.keys(scopeMappingsOptimized).length > 0) { scopesOptimized[scope] = scopeMappingsOptimized; } }); return { imports, scopes: scopesOptimized }; }; const magicExtensions = [".js", ".json", ".node"]; const resolvePackageMain = ({ warn, packageConditions, packageFileUrl, packageJsonObject }) => { // we should remove "module", "browser", "jsenext:main" because Node.js native resolution // ignores them if (packageConditions.includes("import") && "module" in packageJsonObject) { return resolveMainFile({ warn, packageFileUrl, packageMainFieldName: "module", packageMainFieldValue: packageJsonObject.module }); } if (packageConditions.includes("import") && "jsnext:main" in packageJsonObject) { return resolveMainFile({ warn, packageFileUrl, packageMainFieldName: "jsnext:main", packageMainFieldValue: packageJsonObject["jsnext:main"] }); } if (packageConditions.includes("browser") && "browser" in packageJsonObject && // when it's an object it means some files // should be replaced with an other, let's ignore this when we are searching // for the main file typeof packageJsonObject.browser === "string") { return resolveMainFile({ warn, packageFileUrl, packageMainFieldName: "browser", packageMainFieldValue: packageJsonObject.browser }); } if ("main" in packageJsonObject) { return resolveMainFile({ warn, packageFileUrl, packageMainFieldName: "main", packageMainFieldValue: packageJsonObject.main }); } return resolveMainFile({ warn, packageFileUrl, packageMainFieldName: "default", packageMainFieldValue: "index" }); }; const resolveMainFile = async ({ warn, packageFileUrl, packageMainFieldName, packageMainFieldValue }) => { // main is explicitely empty meaning // it is assumed that we should not find a file if (packageMainFieldValue === "") { return null; } const packageDirectoryUrl = util.resolveUrl("./", packageFileUrl); const mainFileRelativeUrl = packageMainFieldValue.endsWith("/") ? `${packageMainFieldValue}index` : packageMainFieldValue; const mainFileUrlFirstCandidate = util.resolveUrl(mainFileRelativeUrl, packageFileUrl); if (!mainFileUrlFirstCandidate.startsWith(packageDirectoryUrl)) { warn(createPackageMainFileMustBeRelativeWarning({ packageMainFieldName, packageMainFieldValue, packageFileUrl })); return null; } const mainFileUrl = await resolveFile(mainFileUrlFirstCandidate, { magicExtensions }); if (!mainFileUrl) { // we know in advance this remapping does not lead to an actual file. // we only warn because we have no guarantee this remapping will actually be used // in the codebase. // warn only if there is actually a main field // otherwise the package.json is missing the main field // it certainly means it's not important if (packageMainFieldName !== "default") { warn(createPackageMainFileNotFoundWarning({ specifier: packageMainFieldValue, importedIn: `${packageFileUrl}#${packageMainFieldName}`, fileUrl: mainFileUrlFirstCandidate, magicExtensions })); } return mainFileUrlFirstCandidate; } return mainFileUrl; }; const createPackageMainFileMustBeRelativeWarning = ({ packageMainFieldName, packageMainFieldValue, packageFileUrl }) => { return { code: "PACKAGE_MAIN_FILE_MUST_BE_RELATIVE", message: `${packageMainFieldName} field in package.json must be inside package.json folder. --- ${packageMainFieldName} --- ${packageMainFieldValue} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; const createPackageMainFileNotFoundWarning = ({ specifier, importedIn, fileUrl, magicExtensions }) => { return { code: "PACKAGE_MAIN_FILE_NOT_FOUND", message: logger.createDetailedMessage(`Cannot find package main file "${specifier}"`, { "imported in": importedIn, "file url tried": fileUrl, ...(util.urlToExtension(fileUrl) === "" ? { ["extensions tried"]: magicExtensions.join(`, `) } : {}) }) }; }; const visitPackageImportMap = async ({ warn, packageFileUrl, packageJsonObject, packageImportmap = packageJsonObject.importmap, projectDirectoryUrl }) => { if (typeof packageImportmap === "undefined") { return {}; } if (typeof packageImportmap === "string") { const importmapFileUrl = importMap.resolveUrl(packageImportmap, packageFileUrl); try { const importmap = await util.readFile(importmapFileUrl, { as: "json" }); return importMap.moveImportMap(importmap, importmapFileUrl, projectDirectoryUrl); } catch (e) { if (e.code === "ENOENT") { warn(createPackageImportMapNotFoundWarning({ importmapFileUrl, packageFileUrl })); return {}; } throw e; } } if (typeof packageImportmap === "object" && packageImportmap !== null) { return packageImportmap; } warn(createPackageImportMapUnexpectedWarning({ packageImportmap, packageFileUrl })); return {}; }; const createPackageImportMapNotFoundWarning = ({ importmapFileUrl, packageFileUrl }) => { return { code: "PACKAGE_IMPORTMAP_NOT_FOUND", message: `importmap file specified in a package.json cannot be found, --- importmap file path --- ${importmapFileUrl} --- package.json path --- ${packageFileUrl}` }; }; const createPackageImportMapUnexpectedWarning = ({ packageImportmap, packageFileUrl }) => { return { code: "PACKAGE_IMPORTMAP_UNEXPECTED", message: `unexpected value in package.json importmap field: value must be a string or an object. --- value --- ${packageImportmap} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; const specifierIsRelative = specifier => { if (specifier.startsWith("//")) { return false; } if (specifier.startsWith("../")) { return false; } // starts with http:// or file:// or ftp: for instance if (/^[a-zA-Z]+\:/.test(specifier)) { return false; } return true; }; /* https://nodejs.org/docs/latest-v15.x/api/packages.html#packages_node_js_package_json_field_definitions */ const visitPackageImports = ({ packageFileUrl, packageJsonObject, packageImports = packageJsonObject.imports, packageConditions, warn }) => { const importsSubpaths = {}; const onImportsSubpath = ({ key, value, trace }) => { if (!specifierIsRelative(value)) { warn(createSubpathValueMustBeRelativeWarning$1({ value, valueTrace: trace, packageFileUrl })); return; } const keyNormalized = key; const valueNormalized = value; importsSubpaths[keyNormalized] = valueNormalized; }; const conditions = [...packageConditions, "default"]; const visitSubpathValue = (subpathValue, subpathValueTrace) => { if (typeof subpathValue === "string") { return handleString(subpathValue, subpathValueTrace); } if (typeof subpathValue === "object" && subpathValue !== null) { return handleObject(subpathValue, subpathValueTrace); } return handleRemaining(subpathValue, subpathValueTrace); }; const handleString = (subpathValue, subpathValueTrace) => { const firstBareKey = subpathValueTrace.slice().reverse().find(key => key.startsWith("#")); onImportsSubpath({ key: firstBareKey, value: subpathValue, trace: subpathValueTrace }); return true; }; const handleObject = (subpathValue, subpathValueTrace) => { // From Node.js documentation: // "If a nested conditional does not have any mapping it will continue // checking the remaining conditions of the parent condition" // https://nodejs.org/docs/latest-v14.x/api/packages.html#packages_nested_conditions // // So it seems what we do here is not sufficient // -> if the condition finally does not lead to something // it should be ignored and an other branch be taken until // something resolves const followConditionBranch = (subpathValue, conditionTrace) => { const bareKeys = []; const conditionalKeys = []; Object.keys(subpathValue).forEach(availableKey => { if (availableKey.startsWith("#")) { bareKeys.push(availableKey); } else { conditionalKeys.push(availableKey); } }); if (bareKeys.length > 0 && conditionalKeys.length > 0) { warn(createSubpathKeysAreMixedWarning$1({ subpathValue, subpathValueTrace: [...subpathValueTrace, ...conditionTrace], packageFileUrl, bareKeys, conditionalKeys })); return false; } // there is no condition, visit all bare keys (starting with #) if (conditionalKeys.length === 0) { let leadsToSomething = false; bareKeys.forEach(key => { leadsToSomething = visitSubpathValue(subpathValue[key], [...subpathValueTrace, ...conditionTrace, key]); }); return leadsToSomething; } // there is a condition, keep the first one leading to something return conditionalKeys.some(keyCandidate => { if (!conditions.includes(keyCandidate)) { return false; } const valueCandidate = subpathValue[keyCandidate]; return visitSubpathValue(valueCandidate, [...subpathValueTrace, ...conditionTrace, keyCandidate]); }); }; return followConditionBranch(subpathValue, []); }; const handleRemaining = (subpathValue, subpathValueTrace) => { warn(createSubpathIsUnexpectedWarning$1({ subpathValue, subpathValueTrace, packageFileUrl })); return false; }; visitSubpathValue(packageImports, ["imports"]); return importsSubpaths; }; const createSubpathIsUnexpectedWarning$1 = ({ subpathValue, subpathValueTrace, packageFileUrl }) => { return { code: "IMPORTS_SUBPATH_UNEXPECTED", message: `unexpected subpath in package.json imports: value must be an object or a string. --- value --- ${subpathValue} --- value at --- ${subpathValueTrace.join(".")} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; const createSubpathKeysAreMixedWarning$1 = ({ subpathValue, subpathValueTrace, packageFileUrl }) => { return { code: "IMPORTS_SUBPATH_MIXED_KEYS", message: `unexpected subpath keys in package.json imports: cannot mix bare and conditional keys. --- value --- ${JSON.stringify(subpathValue, null, " ")} --- value at --- ${subpathValueTrace.join(".")} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; const createSubpathValueMustBeRelativeWarning$1 = ({ value, valueTrace, packageFileUrl }) => { return { code: "IMPORTS_SUBPATH_VALUE_UNEXPECTED", message: `unexpected subpath value in package.json imports: value must be relative to package --- value --- ${value} --- value at --- ${valueTrace.join(".")} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; /* https://nodejs.org/docs/latest-v15.x/api/packages.html#packages_node_js_package_json_field_definitions */ const visitPackageExports = ({ packageFileUrl, packageJsonObject, packageExports = packageJsonObject.exports, packageName = packageJsonObject.name, projectDirectoryUrl, packageConditions, warn }) => { const exportsSubpaths = {}; const packageDirectoryUrl = util.resolveUrl("./", packageFileUrl); const packageDirectoryRelativeUrl = util.urlToRelativeUrl(packageDirectoryUrl, projectDirectoryUrl); const onExportsSubpath = ({ key, value, trace }) => { if (!specifierIsRelative(value)) { warn(createSubpathValueMustBeRelativeWarning({ value, valueTrace: trace, packageFileUrl })); return; } const keyNormalized = specifierToSource(key, packageName); const valueNormalized = addressToDestination(value, packageDirectoryRelativeUrl); exportsSubpaths[keyNormalized] = valueNormalized; }; const conditions = [...packageConditions, "default"]; const visitSubpathValue = (subpathValue, subpathValueTrace) => { // false is allowed as alternative to exports: {} if (subpathValue === false) { return handleFalse(); } if (typeof subpathValue === "string") { return handleString(subpathValue, subpathValueTrace); } if (typeof subpathValue === "object" && subpathValue !== null) { return handleObject(subpathValue, subpathValueTrace); } return handleRemaining(subpathValue, subpathValueTrace); }; const handleFalse = () => { // nothing to do return true; }; const handleString = (subpathValue, subpathValueTrace) => { const firstRelativeKey = subpathValueTrace.slice().reverse().find(key => key.startsWith(".")); const key = firstRelativeKey || "."; onExportsSubpath({ key, value: subpathValue, trace: subpathValueTrace }); return true; }; const handleObject = (subpathValue, subpathValueTrace) => { // From Node.js documentation: // "If a nested conditional does not have any mapping it will continue // checking the remaining conditions of the parent condition" // https://nodejs.org/docs/latest-v14.x/api/packages.html#packages_nested_conditions // // So it seems what we do here is not sufficient // -> if the condition finally does not lead to something // it should be ignored and an other branch be taken until // something resolves const followConditionBranch = (subpathValue, conditionTrace) => { const relativeKeys = []; const conditionalKeys = []; Object.keys(subpathValue).forEach(availableKey => { if (availableKey.startsWith(".")) { relativeKeys.push(availableKey); } else { conditionalKeys.push(availableKey); } }); if (relativeKeys.length > 0 && conditionalKeys.length > 0) { warn(createSubpathKeysAreMixedWarning({ subpathValue, subpathValueTrace: [...subpathValueTrace, ...conditionTrace], packageFileUrl, relativeKeys, conditionalKeys })); return false; } // there is no condition, visit all relative keys if (conditionalKeys.length === 0) { let leadsToSomething = false; relativeKeys.forEach(key => { leadsToSomething = visitSubpathValue(subpathValue[key], [...subpathValueTrace, ...conditionTrace, key]); }); return leadsToSomething; } // there is a condition, keep the first one leading to something return conditionalKeys.some(keyCandidate => { if (!conditions.includes(keyCandidate)) { return false; } const valueCandidate = subpathValue[keyCandidate]; return visitSubpathValue(valueCandidate, [...subpathValueTrace, ...conditionTrace, keyCandidate]); }); }; if (Array.isArray(subpathValue)) { subpathValue = exportsObjectFromExportsArray(subpathValue); } return followConditionBranch(subpathValue, []); }; const handleRemaining = (subpathValue, subpathValueTrace) => { warn(createSubpathIsUnexpectedWarning({ subpathValue, subpathValueTrace, packageFileUrl })); return false; }; visitSubpathValue(packageExports, ["exports"]); return exportsSubpaths; }; const exportsObjectFromExportsArray = exportsArray => { const exportsObject = {}; exportsArray.forEach(exportValue => { if (typeof exportValue === "object") { Object.assign(exportsObject, exportValue); return; } if (typeof exportValue === "string") { exportsObject.default = exportValue; } }); return exportsObject; }; const specifierToSource = (specifier, packageName) => { if (specifier === ".") { return packageName; } if (specifier[0] === "/") { return specifier; } if (specifier.startsWith("./")) { return `${packageName}${specifier.slice(1)}`; } return `${packageName}/${specifier}`; }; const addressToDestination = (address, packageDirectoryRelativeUrl) => { if (address[0] === "/") { return address; } if (address.startsWith("./")) { return `./${packageDirectoryRelativeUrl}${address.slice(2)}`; } return `./${packageDirectoryRelativeUrl}${address}`; }; const createSubpathIsUnexpectedWarning = ({ subpathValue, subpathValueTrace, packageFileUrl }) => { return { code: "EXPORTS_SUBPATH_UNEXPECTED", message: `unexpected subpath in package.json exports: value must be an object or a string. --- value --- ${subpathValue} --- value at --- ${subpathValueTrace.join(".")} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; const createSubpathKeysAreMixedWarning = ({ subpathValue, subpathValueTrace, packageFileUrl }) => { return { code: "EXPORTS_SUBPATH_MIXED_KEYS", message: `unexpected subpath keys in package.json exports: cannot mix relative and conditional keys. --- value --- ${JSON.stringify(subpathValue, null, " ")} --- value at --- ${subpathValueTrace.join(".")} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; const createSubpathValueMustBeRelativeWarning = ({ value, valueTrace, packageFileUrl }) => { return { code: "EXPORTS_SUBPATH_VALUE_MUST_BE_RELATIVE", message: `unexpected subpath value in package.json exports: value must be a relative to the package. --- value --- ${value} --- value at --- ${valueTrace.join(".")} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)}` }; }; const applyPackageManualOverride = (packageObject, packagesManualOverrides) => { const { name, version } = packageObject; const overrideKey = Object.keys(packagesManualOverrides).find(overrideKeyCandidate => { if (name === overrideKeyCandidate) { return true; } if (`${name}@${version}` === overrideKeyCandidate) { return true; } return false; }); if (overrideKey) { return composeObject(packageObject, packagesManualOverrides[overrideKey]); } return packageObject; }; const composeObject = (leftObject, rightObject) => { const composedObject = { ...leftObject }; Object.keys(rightObject).forEach(key => { const rightValue = rightObject[key]; if (rightValue === null || typeof rightValue !== "object" || key in leftObject === false) { composedObject[key] = rightValue; } else { const leftValue = leftObject[key]; if (leftValue === null || typeof leftValue !== "object") { composedObject[key] = rightValue; } else { composedObject[key] = composeObject(leftValue, rightValue); } } }); return composedObject; }; const PACKAGE_NOT_FOUND = {}; const PACKAGE_WITH_SYNTAX_ERROR = {}; const readPackageFile = async (packageFileUrl, packagesManualOverrides) => { try { const packageObject = await util.readFile(packageFileUrl, { as: "json" }); return applyPackageManualOverride(packageObject, packagesManualOverrides); } catch (e) { if (e.code === "ENOENT") { return PACKAGE_NOT_FOUND; } if (e.name === "SyntaxError") { console.error(formatPackageSyntaxErrorLog({ syntaxError: e, packageFileUrl })); return PACKAGE_WITH_SYNTAX_ERROR; } throw e; } }; const formatPackageSyntaxErrorLog = ({ syntaxError, packageFileUrl }) => { return ` error while parsing package.json. --- syntax error message --- ${syntaxError.message} --- package.json path --- ${util.urlToFileSystemPath(packageFileUrl)} `; }; const createFindNodeModulePackage = packagesManualOverrides => { const readPackageFileMemoized = memoizeAsyncFunctionByUrl(packageFileUrl => { return readPackageFile(packageFileUrl, packagesManualOverrides); }); return ({ projectDirectoryUrl, packageFileUrl, dependencyName }) => { const nodeModuleCandidates = getNodeModuleCandidates(packageFileUrl, projectDirectoryUrl); return cancellation.firstOperationMatching({ array: nodeModuleCandidates, start: async nodeModuleCandidate => { const packageFileUrlCandidate = `${projectDirectoryUrl}${nodeModuleCandidate}${dependencyName}/package.json`; const packageObjectCandidate = await readPackageFileMemoized(packageFileUrlCandidate); return { packageFileUrl: packageFileUrlCandidate, packageJsonObject: packageObjectCandidate, syntaxError: packageObjectCandidate === PACKAGE_WITH_SYNTAX_ERROR }; }, predicate: ({ packageJsonObject }) => { return packageJsonObject !== PACKAGE_NOT_FOUND; } }); }; }; const getNodeModuleCandidates = (fileUrl, projectDirectoryUrl) => { const fileDirectoryUrl = util.resolveUrl("./", fileUrl); if (fileDirectoryUrl === projectDirectoryUrl) { return [`node_modules/`]; } const fileDirectoryRelativeUrl = util.urlToRelativeUrl(fileDirectoryUrl, projectDirectoryUrl); const candidates = []; const relativeNodeModuleDirectoryArray = fileDirectoryRelativeUrl.split("node_modules/"); // remove the first empty string relativeNodeModuleDirectoryArray.shift(); let i = relativeNodeModuleDirectoryArray.length; while (i--) { candidates.push(`node_modules/${relativeNodeModuleDirectoryArray.slice(0, i + 1).join("node_modules/")}node_modules/`); } return [...candidates, "node_modules/"]; }; const getImportMapFromPackageFiles = async ({ // nothing is actually listening for this cancellationToken for now // it's not very important but it would be better to register on it // an stops what we are doing if asked to do so // cancellationToken = createCancellationTokenForProcess(), logger, warn, projectDirectoryUrl, projectPackageFileUrl, projectPackageObject, projectPackageDevDependenciesIncluded = process.env.NODE_ENV !== "production", packageConditions = ["import", "browser"], packagesManualOverrides = {}, packageIncludedPredicate = () => true }) => { const findNodeModulePackage = createFindNodeModulePackage(packagesManualOverrides); const imports = {}; const scopes = {}; const addMapping = ({ scope, from, to }) => { if (scope) { // when a package says './' maps to './' // we must add something to say if we are already inside the package // no need to ensure leading slash are scoped to the package if (from === "./" && to === scope) { addMapping({ scope, from: scope, to: scope }); const packageName = scope.slice(scope.lastIndexOf("node_modules/") + `node_modules/`.length); addMapping({ scope, from: packageName, to: scope }); } scopes[scope] = { ...(scopes[scope] || {}), [from]: to }; } else { // we could think it's useless to remap from with to // however it can be used to ensure a weaker remapping // does not win over this specific file or folder if (from === to) { /** * however remapping '/' to '/' is truly useless * moreover it would make wrapImportMap create something like * { * imports: { * "/": "/.dist/best/" * } * } * that would append the wrapped folder twice * */ if (from === "/") return; } imports[from] = to; } }; const seen = {}; const markPackageAsSeen = (packageFileUrl, importerPackageFileUrl) => { if (packageFileUrl in seen) { seen[packageFileUrl].push(importerPackageFileUrl); } else { seen[packageFileUrl] = [importerPackageFileUrl]; } }; const packageIsSeen = (packageFileUrl, importerPackageFileUrl) => { return packageFileUrl in seen && seen[packageFileUrl].includes(importerPackageFileUrl); }; const visit = async ({ packageFileUrl, packageName, packageJsonObject, importerPackageFileUrl, importerPackageJsonObject, includeDevDependencies }) => { if (!packageIncludedPredicate({ packageName, packageFileUrl, packageJsonObject })) { return; } await visitDependencies({ packageFileUrl, packageJsonObject, includeDevDependencies }); await visitPackage({ packageFileUrl, packageName, packageJsonObject, importerPackageFileUrl, importerPackageJsonObject }); }; const visitPackage = async ({ packageFileUrl, packageName, packageJsonObject, importerPackageFileUrl }) => { const packageInfo = computePackageInfo({ packageFileUrl, packageName, importerPackageFileUrl }); const { importerIsRoot, importerRelativeUrl, packageIsRoot, packageDirectoryRelativeUrl // packageDirectoryUrl, // packageDirectoryUrlExpected, } = packageInfo; const addImportMapForPackage = importMap => { if (packageIsRoot) { const { imports = {}, scopes = {} } = importMap; Object.keys(imports).forEach(from => { addMapping({ from, to: imports[from] }); }); Object.keys(scopes).forEach(scope => { const scopeMappings = scopes[scope]; Object.keys(scopeMappings).forEach(key => { addMapping({ scope, from: key, to: scopeMappings[key] }); }); }); return; } const { imports = {}, scopes = {} } = importMap; const scope = `./${packageDirectoryRelativeUrl}`; Object.keys(imports).forEach(from => { const to = imports[from]; const toMoved = moveMappingValue(to, packageFileUrl, projectDirectoryUrl); addMapping({ scope, from, to: toMoved }); }); Object.keys(scop