@jsenv/node-module-import-map
Version:
Generate importmap for node_modules.
1,832 lines (1,602 loc) • 69.7 kB
JavaScript
'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