mirror of https://github.com/pulumi/pulumi.git
278 lines
9.4 KiB
TypeScript
278 lines
9.4 KiB
TypeScript
// Copyright 2016-2022, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import * as upath from "upath";
|
|
|
|
type Exports = string | { [key: string]: SubExports };
|
|
type SubExports = string | { [key: string]: SubExports } | null;
|
|
|
|
type PackageDefinition = {
|
|
name: string;
|
|
exports?: Exports;
|
|
};
|
|
|
|
// TODO[issue] handle https://nodejs.org/api/packages.html#package-entry-points
|
|
//
|
|
// Warning: Introducing the "exports" field prevents consumers of a package from using
|
|
// any entry points that are not defined, including the package.json
|
|
// (e.g. require("your-package/package.json"). This will likely be a breaking change.
|
|
|
|
function getPackageDefinition(path: string): PackageDefinition | undefined {
|
|
try {
|
|
const directories = path.split(upath.sep);
|
|
let last: string | undefined = undefined;
|
|
let lastFullPath: string | undefined = undefined;
|
|
while (directories.length > 0) {
|
|
const curPath = directories.join(upath.sep);
|
|
try {
|
|
lastFullPath = require.resolve(curPath);
|
|
last = curPath;
|
|
} catch (e) {
|
|
// current path is not a module
|
|
}
|
|
directories.pop();
|
|
}
|
|
if (last === undefined || lastFullPath === undefined) {
|
|
throw new Error(`no package.json found for ${path}`);
|
|
}
|
|
const packageDefinitionAbsPath = lastFullPath.slice(0, lastFullPath.indexOf(last)) + last + "/package.json";
|
|
return require(packageDefinitionAbsPath);
|
|
} catch (err) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// a module's implementations are leaves of the document tree.
|
|
function getAllLeafStrings(objectOrPath: SubExports, opts?: RequireOpts): string[] {
|
|
if (objectOrPath === null) {
|
|
// module blacklisted return no implementations
|
|
return [];
|
|
}
|
|
if (typeof objectOrPath === "string") {
|
|
return [objectOrPath];
|
|
}
|
|
const strings: string[] = [];
|
|
for (const [key, value] of Object.entries(objectOrPath)) {
|
|
if (opts && !opts.isRequire && key === "require") {
|
|
continue;
|
|
}
|
|
if (opts && !opts.isImport && key === "import") {
|
|
continue;
|
|
}
|
|
const leaves = getAllLeafStrings(value);
|
|
if (leaves.length === 0) {
|
|
// if there's an environment where this export does not work,
|
|
// don't suggest requires from this match as a more preferable path may
|
|
// match this file.
|
|
return [];
|
|
}
|
|
strings.push(...leaves);
|
|
}
|
|
return strings;
|
|
}
|
|
|
|
// from https://github.com/nodejs/node/blob/b191e66ddf/lib/internal/modules/esm/resolve.js#L686
|
|
function patternKeyCompare(a: string, b: string) {
|
|
const aPatternIndex = a.indexOf("*");
|
|
const bPatternIndex = b.indexOf("*");
|
|
const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
|
|
const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
|
|
if (baseLenA > baseLenB) {
|
|
return -1;
|
|
}
|
|
if (baseLenB > baseLenA) {
|
|
return 1;
|
|
}
|
|
if (aPatternIndex === -1) {
|
|
return 1;
|
|
}
|
|
if (bPatternIndex === -1) {
|
|
return -1;
|
|
}
|
|
if (a.length > b.length) {
|
|
return -1;
|
|
}
|
|
if (b.length > a.length) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
type SrcPrefix = string;
|
|
type Rule = [
|
|
SrcPrefix,
|
|
{
|
|
modPrefix: string;
|
|
modSuffix: string;
|
|
srcSuffix: string;
|
|
},
|
|
];
|
|
|
|
function makeRule(srcPattern: string, modPattern: string): Rule {
|
|
const srcSplit = srcPattern.split("*"); // NodeJS doesn't error out when provided multiple '*'.
|
|
const modSplit = modPattern.split("*");
|
|
if (srcSplit.length > 2 || modSplit.length > 2) {
|
|
// there is undefined behavior on more than 1 "*"
|
|
// see https://github.com/nodejs/node/blob/b191e66ddf/lib/internal/modules/esm/resolve.js#L664
|
|
throw new Error("multiple wildcards in single export target specification");
|
|
}
|
|
const [srcPrefix, srcSuffix] = srcSplit;
|
|
const [modPrefix, modSuffix] = modSplit;
|
|
return [
|
|
srcPrefix,
|
|
{
|
|
modPrefix,
|
|
modSuffix: modSuffix || "",
|
|
srcSuffix: srcSuffix || "",
|
|
},
|
|
];
|
|
}
|
|
|
|
class WildcardMap {
|
|
private map: { [srcPrefix: string]: string };
|
|
private rules: Rule[];
|
|
constructor(matches: [string, string[]][]) {
|
|
this.map = {};
|
|
const rules: Rule[] = [];
|
|
for (const [match, srcPaths] of matches) {
|
|
for (const srcPath of srcPaths) {
|
|
if (srcPath.includes("*")) {
|
|
// wildcard match
|
|
rules.push(makeRule(srcPath, match));
|
|
continue;
|
|
}
|
|
this.map[srcPath] = match;
|
|
}
|
|
}
|
|
this.rules = rules.sort((a, b) => patternKeyCompare(a[0], b[0]));
|
|
}
|
|
get(srcName: string): string | undefined {
|
|
if (this.map[srcName]) {
|
|
return this.map[srcName];
|
|
}
|
|
for (const [srcPrefix, srcRule] of this.rules) {
|
|
if (!srcName.startsWith(srcPrefix) || !srcName.endsWith(srcRule.srcSuffix)) {
|
|
continue;
|
|
}
|
|
|
|
const srcSubpath = srcName.slice(srcPrefix.length, srcName.length - srcRule.srcSuffix.length);
|
|
const result = srcRule.modPrefix + srcSubpath + srcRule.modSuffix;
|
|
return result;
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function isConditionalSugar(exports: Exports, name: string) {
|
|
// exports sugar does not handle mixing ["./path/to/module"] path keys
|
|
// and ["default"|"require"|"import"] conditional keys
|
|
// details https://github.com/nodejs/node/blob/b191e66ddf/lib/internal/modules/esm/resolve.js#L593
|
|
let isSugar = false;
|
|
for (const key of Object.keys(exports)) {
|
|
if (isSugar && key.startsWith(".")) {
|
|
throw new Error(
|
|
`${name}:package.json "exports" cannot contain some keys starting with "." and some not.` +
|
|
" The exports object must either be an object of package subpath keys" +
|
|
" or an object of main entry condition name keys only.",
|
|
);
|
|
}
|
|
if (!key.startsWith(".")) {
|
|
isSugar = true;
|
|
continue;
|
|
}
|
|
}
|
|
return isSugar;
|
|
}
|
|
|
|
class ModuleMap {
|
|
readonly name: string;
|
|
private wildcardMap: WildcardMap;
|
|
constructor(name: string, exports: Exports, opts?: RequireOpts) {
|
|
this.name = name;
|
|
|
|
if (isConditionalSugar(exports, name)) {
|
|
// the exports keys are not paths meaning it is an exports sugar we need to simplify
|
|
exports = { ".": exports };
|
|
}
|
|
|
|
const rules: [string, string[]][] = [];
|
|
for (const [modPath, objectOrPath] of Object.entries(exports)) {
|
|
const modName: string = name + modPath.slice(1);
|
|
const leaves = getAllLeafStrings(objectOrPath, opts);
|
|
rules.push([modName, leaves.map((leaf) => name + leaf.slice(1))]);
|
|
}
|
|
this.wildcardMap = new WildcardMap(rules);
|
|
}
|
|
get(srcName: string) {
|
|
const modPath = this.wildcardMap.get(srcName);
|
|
if (modPath === undefined) {
|
|
throw new Error(`package.json export path for "${srcName}" not found`);
|
|
}
|
|
return modPath;
|
|
}
|
|
}
|
|
|
|
type RequireOpts = {
|
|
isRequire?: boolean;
|
|
isImport?: boolean;
|
|
};
|
|
|
|
/*
|
|
We need to resolve from a source file path to a valid module export.
|
|
|
|
Exports to source file is a many-to-one relationship. Reversing this is a one-to-many relationship.
|
|
Any of the initial exports are aliases to the same module and
|
|
we assume to be semantically equivalent. This makes it a one-to-any relationship.
|
|
for example,
|
|
<./package.json>
|
|
"exports": {
|
|
"./foo.js": "./lib/index.js",
|
|
"./bar.js": "./lib/index.js",
|
|
}
|
|
we will resolve ./lib/index.js into either ./foo.js or ./bar.js
|
|
a module can resolve into many files conditionally, but aliases are treated as equivalent.
|
|
|
|
Due to null specifiers for modules and this one-to-many relationship, we assume that anything with a
|
|
null specifier may be unreachable on a different platform and opt for a different alias to cover it if
|
|
it exists.
|
|
|
|
Exports ending in "/" will be deprecated by node.
|
|
|
|
|
|
For more details https://nodejs.org/api/esm.html#resolution-algorithm
|
|
*/
|
|
|
|
export function getModuleFromPath(
|
|
path: string,
|
|
packageDefinition?: PackageDefinition,
|
|
opts: RequireOpts = { isRequire: true },
|
|
) {
|
|
packageDefinition = packageDefinition || getPackageDefinition(path);
|
|
if (packageDefinition === undefined) {
|
|
return path;
|
|
}
|
|
if (packageDefinition.exports === undefined) {
|
|
return path;
|
|
}
|
|
if (typeof packageDefinition.exports === "string") {
|
|
return packageDefinition.name;
|
|
}
|
|
if (typeof packageDefinition.exports === "object") {
|
|
const modMap = new ModuleMap(packageDefinition.name, packageDefinition.exports, opts);
|
|
const modulePath = modMap.get(path);
|
|
return modulePath;
|
|
}
|
|
return path;
|
|
}
|