mirror of
https://github.com/denoland/deno.git
synced 2024-11-27 16:10:57 -05:00
846 lines
23 KiB
TypeScript
846 lines
23 KiB
TypeScript
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||
|
// Copyright Joyent, Inc. and other Node contributors.
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||
|
// copy of this software and associated documentation files (the
|
||
|
// "Software"), to deal in the Software without restriction, including
|
||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||
|
// persons to whom the Software is furnished to do so, subject to the
|
||
|
// following conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be included
|
||
|
// in all copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
|
||
|
/**
|
||
|
* NOTE(bartlomieju):
|
||
|
* Functionality of this file is ported in Rust in `cli/compat/esm_resolver.ts`.
|
||
|
* Unfortunately we have no way to call ESM resolution in Rust from TypeScript code.
|
||
|
*/
|
||
|
|
||
|
import {
|
||
|
fileURLToPath,
|
||
|
pathToFileURL,
|
||
|
} from "internal:deno_node/polyfills/url.ts";
|
||
|
import {
|
||
|
ERR_INVALID_MODULE_SPECIFIER,
|
||
|
ERR_INVALID_PACKAGE_CONFIG,
|
||
|
ERR_INVALID_PACKAGE_TARGET,
|
||
|
ERR_MODULE_NOT_FOUND,
|
||
|
ERR_PACKAGE_IMPORT_NOT_DEFINED,
|
||
|
ERR_PACKAGE_PATH_NOT_EXPORTED,
|
||
|
NodeError,
|
||
|
} from "internal:deno_node/polyfills/internal/errors.ts";
|
||
|
|
||
|
const { hasOwn } = Object;
|
||
|
|
||
|
export const encodedSepRegEx = /%2F|%2C/i;
|
||
|
|
||
|
function throwInvalidSubpath(
|
||
|
subpath: string,
|
||
|
packageJSONUrl: string,
|
||
|
internal: boolean,
|
||
|
base: string,
|
||
|
) {
|
||
|
const reason = `request is not a valid subpath for the "${
|
||
|
internal ? "imports" : "exports"
|
||
|
}" resolution of ${fileURLToPath(packageJSONUrl)}`;
|
||
|
throw new ERR_INVALID_MODULE_SPECIFIER(
|
||
|
subpath,
|
||
|
reason,
|
||
|
base && fileURLToPath(base),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function throwInvalidPackageTarget(
|
||
|
subpath: string,
|
||
|
// deno-lint-ignore no-explicit-any
|
||
|
target: any,
|
||
|
packageJSONUrl: string,
|
||
|
internal: boolean,
|
||
|
base: string,
|
||
|
) {
|
||
|
if (typeof target === "object" && target !== null) {
|
||
|
target = JSON.stringify(target, null, "");
|
||
|
} else {
|
||
|
target = `${target}`;
|
||
|
}
|
||
|
throw new ERR_INVALID_PACKAGE_TARGET(
|
||
|
fileURLToPath(new URL(".", packageJSONUrl)),
|
||
|
subpath,
|
||
|
target,
|
||
|
internal,
|
||
|
base && fileURLToPath(base),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function throwImportNotDefined(
|
||
|
specifier: string,
|
||
|
packageJSONUrl: URL | undefined,
|
||
|
base: string | URL,
|
||
|
): TypeError & { code: string } {
|
||
|
throw new ERR_PACKAGE_IMPORT_NOT_DEFINED(
|
||
|
specifier,
|
||
|
packageJSONUrl && fileURLToPath(new URL(".", packageJSONUrl)),
|
||
|
fileURLToPath(base),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function throwExportsNotFound(
|
||
|
subpath: string,
|
||
|
packageJSONUrl: string,
|
||
|
base?: string,
|
||
|
): Error & { code: string } {
|
||
|
throw new ERR_PACKAGE_PATH_NOT_EXPORTED(
|
||
|
subpath,
|
||
|
fileURLToPath(new URL(".", packageJSONUrl)),
|
||
|
base && fileURLToPath(base),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function patternKeyCompare(a: string, b: string): number {
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
function fileExists(url: string | URL): boolean {
|
||
|
try {
|
||
|
const info = Deno.statSync(url);
|
||
|
return info.isFile;
|
||
|
} catch {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function tryStatSync(path: string): { isDirectory: boolean } {
|
||
|
try {
|
||
|
const info = Deno.statSync(path);
|
||
|
return { isDirectory: info.isDirectory };
|
||
|
} catch {
|
||
|
return { isDirectory: false };
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Legacy CommonJS main resolution:
|
||
|
* 1. let M = pkg_url + (json main field)
|
||
|
* 2. TRY(M, M.js, M.json, M.node)
|
||
|
* 3. TRY(M/index.js, M/index.json, M/index.node)
|
||
|
* 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node)
|
||
|
* 5. NOT_FOUND
|
||
|
*/
|
||
|
function legacyMainResolve(
|
||
|
packageJSONUrl: URL,
|
||
|
packageConfig: PackageConfig,
|
||
|
base: string | URL,
|
||
|
): URL {
|
||
|
let guess;
|
||
|
if (packageConfig.main !== undefined) {
|
||
|
// Note: fs check redundances will be handled by Descriptor cache here.
|
||
|
if (
|
||
|
fileExists(guess = new URL(`./${packageConfig.main}`, packageJSONUrl))
|
||
|
) {
|
||
|
return guess;
|
||
|
} else if (
|
||
|
fileExists(guess = new URL(`./${packageConfig.main}.js`, packageJSONUrl))
|
||
|
) {
|
||
|
// pass
|
||
|
} else if (
|
||
|
fileExists(
|
||
|
guess = new URL(`./${packageConfig.main}.json`, packageJSONUrl),
|
||
|
)
|
||
|
) {
|
||
|
// pass
|
||
|
} else if (
|
||
|
fileExists(
|
||
|
guess = new URL(`./${packageConfig.main}.node`, packageJSONUrl),
|
||
|
)
|
||
|
) {
|
||
|
// pass
|
||
|
} else if (
|
||
|
fileExists(
|
||
|
guess = new URL(`./${packageConfig.main}/index.js`, packageJSONUrl),
|
||
|
)
|
||
|
) {
|
||
|
// pass
|
||
|
} else if (
|
||
|
fileExists(
|
||
|
guess = new URL(`./${packageConfig.main}/index.json`, packageJSONUrl),
|
||
|
)
|
||
|
) {
|
||
|
// pass
|
||
|
} else if (
|
||
|
fileExists(
|
||
|
guess = new URL(`./${packageConfig.main}/index.node`, packageJSONUrl),
|
||
|
)
|
||
|
) {
|
||
|
// pass
|
||
|
} else guess = undefined;
|
||
|
if (guess) {
|
||
|
// TODO(bartlomieju):
|
||
|
// emitLegacyIndexDeprecation(guess, packageJSONUrl, base,
|
||
|
// packageConfig.main);
|
||
|
return guess;
|
||
|
}
|
||
|
// Fallthrough.
|
||
|
}
|
||
|
if (fileExists(guess = new URL("./index.js", packageJSONUrl))) {
|
||
|
// pass
|
||
|
} // So fs.
|
||
|
else if (fileExists(guess = new URL("./index.json", packageJSONUrl))) {
|
||
|
// pass
|
||
|
} else if (fileExists(guess = new URL("./index.node", packageJSONUrl))) {
|
||
|
// pass
|
||
|
} else guess = undefined;
|
||
|
if (guess) {
|
||
|
// TODO(bartlomieju):
|
||
|
// emitLegacyIndexDeprecation(guess, packageJSONUrl, base, packageConfig.main);
|
||
|
return guess;
|
||
|
}
|
||
|
// Not found.
|
||
|
throw new ERR_MODULE_NOT_FOUND(
|
||
|
fileURLToPath(new URL(".", packageJSONUrl)),
|
||
|
fileURLToPath(base),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function parsePackageName(
|
||
|
specifier: string,
|
||
|
base: string | URL,
|
||
|
): { packageName: string; packageSubpath: string; isScoped: boolean } {
|
||
|
let separatorIndex = specifier.indexOf("/");
|
||
|
let validPackageName = true;
|
||
|
let isScoped = false;
|
||
|
if (specifier[0] === "@") {
|
||
|
isScoped = true;
|
||
|
if (separatorIndex === -1 || specifier.length === 0) {
|
||
|
validPackageName = false;
|
||
|
} else {
|
||
|
separatorIndex = specifier.indexOf("/", separatorIndex + 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const packageName = separatorIndex === -1
|
||
|
? specifier
|
||
|
: specifier.slice(0, separatorIndex);
|
||
|
|
||
|
// Package name cannot have leading . and cannot have percent-encoding or
|
||
|
// separators.
|
||
|
for (let i = 0; i < packageName.length; i++) {
|
||
|
if (packageName[i] === "%" || packageName[i] === "\\") {
|
||
|
validPackageName = false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!validPackageName) {
|
||
|
throw new ERR_INVALID_MODULE_SPECIFIER(
|
||
|
specifier,
|
||
|
"is not a valid package name",
|
||
|
fileURLToPath(base),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
const packageSubpath = "." +
|
||
|
(separatorIndex === -1 ? "" : specifier.slice(separatorIndex));
|
||
|
|
||
|
return { packageName, packageSubpath, isScoped };
|
||
|
}
|
||
|
|
||
|
function packageResolve(
|
||
|
specifier: string,
|
||
|
base: string,
|
||
|
conditions: Set<string>,
|
||
|
): URL | undefined {
|
||
|
const { packageName, packageSubpath, isScoped } = parsePackageName(
|
||
|
specifier,
|
||
|
base,
|
||
|
);
|
||
|
|
||
|
// ResolveSelf
|
||
|
const packageConfig = getPackageScopeConfig(base);
|
||
|
if (packageConfig.exists) {
|
||
|
const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
|
||
|
if (
|
||
|
packageConfig.name === packageName &&
|
||
|
packageConfig.exports !== undefined && packageConfig.exports !== null
|
||
|
) {
|
||
|
return packageExportsResolve(
|
||
|
packageJSONUrl.toString(),
|
||
|
packageSubpath,
|
||
|
packageConfig,
|
||
|
base,
|
||
|
conditions,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let packageJSONUrl = new URL(
|
||
|
"./node_modules/" + packageName + "/package.json",
|
||
|
base,
|
||
|
);
|
||
|
let packageJSONPath = fileURLToPath(packageJSONUrl);
|
||
|
let lastPath;
|
||
|
do {
|
||
|
const stat = tryStatSync(
|
||
|
packageJSONPath.slice(0, packageJSONPath.length - 13),
|
||
|
);
|
||
|
if (!stat.isDirectory) {
|
||
|
lastPath = packageJSONPath;
|
||
|
packageJSONUrl = new URL(
|
||
|
(isScoped ? "../../../../node_modules/" : "../../../node_modules/") +
|
||
|
packageName + "/package.json",
|
||
|
packageJSONUrl,
|
||
|
);
|
||
|
packageJSONPath = fileURLToPath(packageJSONUrl);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Package match.
|
||
|
const packageConfig = getPackageConfig(packageJSONPath, specifier, base);
|
||
|
if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
|
||
|
return packageExportsResolve(
|
||
|
packageJSONUrl.toString(),
|
||
|
packageSubpath,
|
||
|
packageConfig,
|
||
|
base,
|
||
|
conditions,
|
||
|
);
|
||
|
}
|
||
|
if (packageSubpath === ".") {
|
||
|
return legacyMainResolve(packageJSONUrl, packageConfig, base);
|
||
|
}
|
||
|
return new URL(packageSubpath, packageJSONUrl);
|
||
|
// Cross-platform root check.
|
||
|
} while (packageJSONPath.length !== lastPath.length);
|
||
|
|
||
|
// TODO(bartlomieju): this is false positive
|
||
|
// deno-lint-ignore no-unreachable
|
||
|
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base));
|
||
|
}
|
||
|
|
||
|
const invalidSegmentRegEx = /(^|\\|\/)(\.\.?|node_modules)(\\|\/|$)/;
|
||
|
const patternRegEx = /\*/g;
|
||
|
|
||
|
function resolvePackageTargetString(
|
||
|
target: string,
|
||
|
subpath: string,
|
||
|
match: string,
|
||
|
packageJSONUrl: string,
|
||
|
base: string,
|
||
|
pattern: boolean,
|
||
|
internal: boolean,
|
||
|
conditions: Set<string>,
|
||
|
): URL | undefined {
|
||
|
if (subpath !== "" && !pattern && target[target.length - 1] !== "/") {
|
||
|
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
||
|
}
|
||
|
|
||
|
if (!target.startsWith("./")) {
|
||
|
if (
|
||
|
internal && !target.startsWith("../") &&
|
||
|
!target.startsWith("/")
|
||
|
) {
|
||
|
let isURL = false;
|
||
|
try {
|
||
|
new URL(target);
|
||
|
isURL = true;
|
||
|
} catch {
|
||
|
// pass
|
||
|
}
|
||
|
if (!isURL) {
|
||
|
const exportTarget = pattern
|
||
|
? target.replace(patternRegEx, () => subpath)
|
||
|
: target + subpath;
|
||
|
return packageResolve(exportTarget, packageJSONUrl, conditions);
|
||
|
}
|
||
|
}
|
||
|
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
||
|
}
|
||
|
|
||
|
if (invalidSegmentRegEx.test(target.slice(2))) {
|
||
|
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
||
|
}
|
||
|
|
||
|
const resolved = new URL(target, packageJSONUrl);
|
||
|
const resolvedPath = resolved.pathname;
|
||
|
const packagePath = new URL(".", packageJSONUrl).pathname;
|
||
|
|
||
|
if (!resolvedPath.startsWith(packagePath)) {
|
||
|
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
|
||
|
}
|
||
|
|
||
|
if (subpath === "") return resolved;
|
||
|
|
||
|
if (invalidSegmentRegEx.test(subpath)) {
|
||
|
const request = pattern
|
||
|
? match.replace("*", () => subpath)
|
||
|
: match + subpath;
|
||
|
throwInvalidSubpath(request, packageJSONUrl, internal, base);
|
||
|
}
|
||
|
|
||
|
if (pattern) {
|
||
|
return new URL(resolved.href.replace(patternRegEx, () => subpath));
|
||
|
}
|
||
|
return new URL(subpath, resolved);
|
||
|
}
|
||
|
|
||
|
function isArrayIndex(key: string): boolean {
|
||
|
const keyNum = +key;
|
||
|
if (`${keyNum}` !== key) return false;
|
||
|
return keyNum >= 0 && keyNum < 0xFFFF_FFFF;
|
||
|
}
|
||
|
|
||
|
function resolvePackageTarget(
|
||
|
packageJSONUrl: string,
|
||
|
// deno-lint-ignore no-explicit-any
|
||
|
target: any,
|
||
|
subpath: string,
|
||
|
packageSubpath: string,
|
||
|
base: string,
|
||
|
pattern: boolean,
|
||
|
internal: boolean,
|
||
|
conditions: Set<string>,
|
||
|
): URL | undefined {
|
||
|
if (typeof target === "string") {
|
||
|
return resolvePackageTargetString(
|
||
|
target,
|
||
|
subpath,
|
||
|
packageSubpath,
|
||
|
packageJSONUrl,
|
||
|
base,
|
||
|
pattern,
|
||
|
internal,
|
||
|
conditions,
|
||
|
);
|
||
|
} else if (Array.isArray(target)) {
|
||
|
if (target.length === 0) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
let lastException;
|
||
|
for (let i = 0; i < target.length; i++) {
|
||
|
const targetItem = target[i];
|
||
|
let resolved;
|
||
|
try {
|
||
|
resolved = resolvePackageTarget(
|
||
|
packageJSONUrl,
|
||
|
targetItem,
|
||
|
subpath,
|
||
|
packageSubpath,
|
||
|
base,
|
||
|
pattern,
|
||
|
internal,
|
||
|
conditions,
|
||
|
);
|
||
|
} catch (e: unknown) {
|
||
|
lastException = e;
|
||
|
if (e instanceof NodeError && e.code === "ERR_INVALID_PACKAGE_TARGET") {
|
||
|
continue;
|
||
|
}
|
||
|
throw e;
|
||
|
}
|
||
|
if (resolved === undefined) {
|
||
|
continue;
|
||
|
}
|
||
|
if (resolved === null) {
|
||
|
lastException = null;
|
||
|
continue;
|
||
|
}
|
||
|
return resolved;
|
||
|
}
|
||
|
if (lastException === undefined || lastException === null) {
|
||
|
return undefined;
|
||
|
}
|
||
|
throw lastException;
|
||
|
} else if (typeof target === "object" && target !== null) {
|
||
|
const keys = Object.getOwnPropertyNames(target);
|
||
|
for (let i = 0; i < keys.length; i++) {
|
||
|
const key = keys[i];
|
||
|
if (isArrayIndex(key)) {
|
||
|
throw new ERR_INVALID_PACKAGE_CONFIG(
|
||
|
fileURLToPath(packageJSONUrl),
|
||
|
base,
|
||
|
'"exports" cannot contain numeric property keys.',
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
for (let i = 0; i < keys.length; i++) {
|
||
|
const key = keys[i];
|
||
|
if (key === "default" || conditions.has(key)) {
|
||
|
const conditionalTarget = target[key];
|
||
|
const resolved = resolvePackageTarget(
|
||
|
packageJSONUrl,
|
||
|
conditionalTarget,
|
||
|
subpath,
|
||
|
packageSubpath,
|
||
|
base,
|
||
|
pattern,
|
||
|
internal,
|
||
|
conditions,
|
||
|
);
|
||
|
if (resolved === undefined) {
|
||
|
continue;
|
||
|
}
|
||
|
return resolved;
|
||
|
}
|
||
|
}
|
||
|
return undefined;
|
||
|
} else if (target === null) {
|
||
|
return undefined;
|
||
|
}
|
||
|
throwInvalidPackageTarget(
|
||
|
packageSubpath,
|
||
|
target,
|
||
|
packageJSONUrl,
|
||
|
internal,
|
||
|
base,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export function packageExportsResolve(
|
||
|
packageJSONUrl: string,
|
||
|
packageSubpath: string,
|
||
|
packageConfig: PackageConfig,
|
||
|
base: string,
|
||
|
conditions: Set<string>,
|
||
|
// @ts-ignore `URL` needs to be forced due to control flow
|
||
|
): URL {
|
||
|
let exports = packageConfig.exports;
|
||
|
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
|
||
|
exports = { ".": exports };
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
hasOwn(exports, packageSubpath) &&
|
||
|
!packageSubpath.includes("*") &&
|
||
|
!packageSubpath.endsWith("/")
|
||
|
) {
|
||
|
const target = exports[packageSubpath];
|
||
|
const resolved = resolvePackageTarget(
|
||
|
packageJSONUrl,
|
||
|
target,
|
||
|
"",
|
||
|
packageSubpath,
|
||
|
base,
|
||
|
false,
|
||
|
false,
|
||
|
conditions,
|
||
|
);
|
||
|
if (resolved === null || resolved === undefined) {
|
||
|
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
||
|
}
|
||
|
return resolved!;
|
||
|
}
|
||
|
|
||
|
let bestMatch = "";
|
||
|
let bestMatchSubpath = "";
|
||
|
const keys = Object.getOwnPropertyNames(exports);
|
||
|
for (let i = 0; i < keys.length; i++) {
|
||
|
const key = keys[i];
|
||
|
const patternIndex = key.indexOf("*");
|
||
|
if (
|
||
|
patternIndex !== -1 &&
|
||
|
packageSubpath.startsWith(key.slice(0, patternIndex))
|
||
|
) {
|
||
|
// When this reaches EOL, this can throw at the top of the whole function:
|
||
|
//
|
||
|
// if (StringPrototypeEndsWith(packageSubpath, '/'))
|
||
|
// throwInvalidSubpath(packageSubpath)
|
||
|
//
|
||
|
// To match "imports" and the spec.
|
||
|
if (packageSubpath.endsWith("/")) {
|
||
|
// TODO(@bartlomieju):
|
||
|
// emitTrailingSlashPatternDeprecation(
|
||
|
// packageSubpath,
|
||
|
// packageJSONUrl,
|
||
|
// base,
|
||
|
// );
|
||
|
}
|
||
|
const patternTrailer = key.slice(patternIndex + 1);
|
||
|
if (
|
||
|
packageSubpath.length >= key.length &&
|
||
|
packageSubpath.endsWith(patternTrailer) &&
|
||
|
patternKeyCompare(bestMatch, key) === 1 &&
|
||
|
key.lastIndexOf("*") === patternIndex
|
||
|
) {
|
||
|
bestMatch = key;
|
||
|
bestMatchSubpath = packageSubpath.slice(
|
||
|
patternIndex,
|
||
|
packageSubpath.length - patternTrailer.length,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bestMatch) {
|
||
|
const target = exports[bestMatch];
|
||
|
const resolved = resolvePackageTarget(
|
||
|
packageJSONUrl,
|
||
|
target,
|
||
|
bestMatchSubpath,
|
||
|
bestMatch,
|
||
|
base,
|
||
|
true,
|
||
|
false,
|
||
|
conditions,
|
||
|
);
|
||
|
if (resolved === null || resolved === undefined) {
|
||
|
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
||
|
}
|
||
|
return resolved!;
|
||
|
}
|
||
|
|
||
|
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
|
||
|
}
|
||
|
|
||
|
export interface PackageConfig {
|
||
|
pjsonPath: string;
|
||
|
exists: boolean;
|
||
|
name?: string;
|
||
|
main?: string;
|
||
|
// deno-lint-ignore no-explicit-any
|
||
|
exports?: any;
|
||
|
// deno-lint-ignore no-explicit-any
|
||
|
imports?: any;
|
||
|
type?: string;
|
||
|
}
|
||
|
|
||
|
const packageJSONCache = new Map(); /* string -> PackageConfig */
|
||
|
|
||
|
function getPackageConfig(
|
||
|
path: string,
|
||
|
specifier: string | URL,
|
||
|
base?: string | URL,
|
||
|
): PackageConfig {
|
||
|
const existing = packageJSONCache.get(path);
|
||
|
if (existing !== undefined) {
|
||
|
return existing;
|
||
|
}
|
||
|
|
||
|
let source: string | undefined;
|
||
|
try {
|
||
|
source = new TextDecoder().decode(
|
||
|
Deno.readFileSync(path),
|
||
|
);
|
||
|
} catch {
|
||
|
// pass
|
||
|
}
|
||
|
|
||
|
if (source === undefined) {
|
||
|
const packageConfig = {
|
||
|
pjsonPath: path,
|
||
|
exists: false,
|
||
|
main: undefined,
|
||
|
name: undefined,
|
||
|
type: "none",
|
||
|
exports: undefined,
|
||
|
imports: undefined,
|
||
|
};
|
||
|
packageJSONCache.set(path, packageConfig);
|
||
|
return packageConfig;
|
||
|
}
|
||
|
|
||
|
let packageJSON;
|
||
|
try {
|
||
|
packageJSON = JSON.parse(source);
|
||
|
} catch (error) {
|
||
|
throw new ERR_INVALID_PACKAGE_CONFIG(
|
||
|
path,
|
||
|
(base ? `"${specifier}" from ` : "") + fileURLToPath(base || specifier),
|
||
|
// @ts-ignore there's no assertion for type and `error` is thus `unknown`
|
||
|
error.message,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
let { imports, main, name, type } = packageJSON;
|
||
|
const { exports } = packageJSON;
|
||
|
if (typeof imports !== "object" || imports === null) imports = undefined;
|
||
|
if (typeof main !== "string") main = undefined;
|
||
|
if (typeof name !== "string") name = undefined;
|
||
|
// Ignore unknown types for forwards compatibility
|
||
|
if (type !== "module" && type !== "commonjs") type = "none";
|
||
|
|
||
|
const packageConfig = {
|
||
|
pjsonPath: path,
|
||
|
exists: true,
|
||
|
main,
|
||
|
name,
|
||
|
type,
|
||
|
exports,
|
||
|
imports,
|
||
|
};
|
||
|
packageJSONCache.set(path, packageConfig);
|
||
|
return packageConfig;
|
||
|
}
|
||
|
|
||
|
function getPackageScopeConfig(resolved: URL | string): PackageConfig {
|
||
|
let packageJSONUrl = new URL("./package.json", resolved);
|
||
|
while (true) {
|
||
|
const packageJSONPath = packageJSONUrl.pathname;
|
||
|
if (packageJSONPath.endsWith("node_modules/package.json")) {
|
||
|
break;
|
||
|
}
|
||
|
const packageConfig = getPackageConfig(
|
||
|
fileURLToPath(packageJSONUrl),
|
||
|
resolved,
|
||
|
);
|
||
|
if (packageConfig.exists) return packageConfig;
|
||
|
|
||
|
const lastPackageJSONUrl = packageJSONUrl;
|
||
|
packageJSONUrl = new URL("../package.json", packageJSONUrl);
|
||
|
|
||
|
// Terminates at root where ../package.json equals ../../package.json
|
||
|
// (can't just check "/package.json" for Windows support).
|
||
|
if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break;
|
||
|
}
|
||
|
const packageJSONPath = fileURLToPath(packageJSONUrl);
|
||
|
const packageConfig = {
|
||
|
pjsonPath: packageJSONPath,
|
||
|
exists: false,
|
||
|
main: undefined,
|
||
|
name: undefined,
|
||
|
type: "none",
|
||
|
exports: undefined,
|
||
|
imports: undefined,
|
||
|
};
|
||
|
packageJSONCache.set(packageJSONPath, packageConfig);
|
||
|
return packageConfig;
|
||
|
}
|
||
|
|
||
|
export function packageImportsResolve(
|
||
|
name: string,
|
||
|
base: string,
|
||
|
conditions: Set<string>,
|
||
|
// @ts-ignore `URL` needs to be forced due to control flow
|
||
|
): URL {
|
||
|
if (
|
||
|
name === "#" || name.startsWith("#/") ||
|
||
|
name.startsWith("/")
|
||
|
) {
|
||
|
const reason = "is not a valid internal imports specifier name";
|
||
|
throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base));
|
||
|
}
|
||
|
let packageJSONUrl;
|
||
|
const packageConfig = getPackageScopeConfig(base);
|
||
|
if (packageConfig.exists) {
|
||
|
packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
|
||
|
const imports = packageConfig.imports;
|
||
|
if (imports) {
|
||
|
if (
|
||
|
hasOwn(imports, name) &&
|
||
|
!name.includes("*")
|
||
|
) {
|
||
|
const resolved = resolvePackageTarget(
|
||
|
packageJSONUrl.toString(),
|
||
|
imports[name],
|
||
|
"",
|
||
|
name,
|
||
|
base,
|
||
|
false,
|
||
|
true,
|
||
|
conditions,
|
||
|
);
|
||
|
if (resolved !== null && resolved !== undefined) {
|
||
|
return resolved;
|
||
|
}
|
||
|
} else {
|
||
|
let bestMatch = "";
|
||
|
let bestMatchSubpath = "";
|
||
|
const keys = Object.getOwnPropertyNames(imports);
|
||
|
for (let i = 0; i < keys.length; i++) {
|
||
|
const key = keys[i];
|
||
|
const patternIndex = key.indexOf("*");
|
||
|
if (
|
||
|
patternIndex !== -1 &&
|
||
|
name.startsWith(
|
||
|
key.slice(0, patternIndex),
|
||
|
)
|
||
|
) {
|
||
|
const patternTrailer = key.slice(patternIndex + 1);
|
||
|
if (
|
||
|
name.length >= key.length &&
|
||
|
name.endsWith(patternTrailer) &&
|
||
|
patternKeyCompare(bestMatch, key) === 1 &&
|
||
|
key.lastIndexOf("*") === patternIndex
|
||
|
) {
|
||
|
bestMatch = key;
|
||
|
bestMatchSubpath = name.slice(
|
||
|
patternIndex,
|
||
|
name.length - patternTrailer.length,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bestMatch) {
|
||
|
const target = imports[bestMatch];
|
||
|
const resolved = resolvePackageTarget(
|
||
|
packageJSONUrl.toString(),
|
||
|
target,
|
||
|
bestMatchSubpath,
|
||
|
bestMatch,
|
||
|
base,
|
||
|
true,
|
||
|
true,
|
||
|
conditions,
|
||
|
);
|
||
|
if (resolved !== null && resolved !== undefined) {
|
||
|
return resolved;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
throwImportNotDefined(name, packageJSONUrl, base);
|
||
|
}
|
||
|
|
||
|
function isConditionalExportsMainSugar(
|
||
|
// deno-lint-ignore no-explicit-any
|
||
|
exports: any,
|
||
|
packageJSONUrl: string,
|
||
|
base: string,
|
||
|
): boolean {
|
||
|
if (typeof exports === "string" || Array.isArray(exports)) return true;
|
||
|
if (typeof exports !== "object" || exports === null) return false;
|
||
|
|
||
|
const keys = Object.getOwnPropertyNames(exports);
|
||
|
let isConditionalSugar = false;
|
||
|
let i = 0;
|
||
|
for (let j = 0; j < keys.length; j++) {
|
||
|
const key = keys[j];
|
||
|
const curIsConditionalSugar = key === "" || key[0] !== ".";
|
||
|
if (i++ === 0) {
|
||
|
isConditionalSugar = curIsConditionalSugar;
|
||
|
} else if (isConditionalSugar !== curIsConditionalSugar) {
|
||
|
const message =
|
||
|
"\"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.";
|
||
|
throw new ERR_INVALID_PACKAGE_CONFIG(
|
||
|
fileURLToPath(packageJSONUrl),
|
||
|
base,
|
||
|
message,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
return isConditionalSugar;
|
||
|
}
|