1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

refactor(core): InternalModuleLoader checks if all files were used (#18005)

This commit changes "InternalModuleLoader" from "deno_core" to 
store a list of used modules during snapshotting. If a module was not
used during snapshotting "InternalModuleLoader" will panic in its "Drop"
handler signaling to the embedder that they made a mistake somewhere.
This commit is contained in:
Bartek Iwańczuk 2023-03-05 18:42:52 -04:00 committed by GitHub
parent 5f34c9be91
commit 1ab16e2426
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 37 additions and 1196 deletions

View file

@ -328,6 +328,7 @@ pub type InternalModuleLoaderCb =
pub struct InternalModuleLoader {
module_loader: Rc<dyn ModuleLoader>,
esm_sources: Vec<ExtensionFileSource>,
used_esm_sources: RefCell<HashMap<String, bool>>,
maybe_load_callback: Option<InternalModuleLoaderCb>,
}
@ -336,6 +337,7 @@ impl Default for InternalModuleLoader {
Self {
module_loader: Rc::new(NoopModuleLoader),
esm_sources: vec![],
used_esm_sources: RefCell::new(HashMap::default()),
maybe_load_callback: None,
}
}
@ -347,14 +349,43 @@ impl InternalModuleLoader {
esm_sources: Vec<ExtensionFileSource>,
maybe_load_callback: Option<InternalModuleLoaderCb>,
) -> Self {
let used_esm_sources: HashMap<String, bool> = esm_sources
.iter()
.map(|file_source| (file_source.specifier.to_string(), false))
.collect();
InternalModuleLoader {
module_loader: module_loader.unwrap_or_else(|| Rc::new(NoopModuleLoader)),
esm_sources,
used_esm_sources: RefCell::new(used_esm_sources),
maybe_load_callback,
}
}
}
impl Drop for InternalModuleLoader {
fn drop(&mut self) {
let used_esm_sources = self.used_esm_sources.get_mut();
let unused_modules: Vec<_> = used_esm_sources
.iter()
.filter(|(_s, v)| !*v)
.map(|(s, _)| s)
.collect();
if !unused_modules.is_empty() {
let mut msg =
"Following modules were passed to InternalModuleLoader but never used:\n"
.to_string();
for m in unused_modules {
msg.push_str(" - ");
msg.push_str(m);
msg.push('\n');
}
panic!("{}", msg);
}
}
}
impl ModuleLoader for InternalModuleLoader {
fn resolve(
&self,
@ -400,6 +431,12 @@ impl ModuleLoader for InternalModuleLoader {
.find(|file_source| file_source.specifier == module_specifier.as_str());
if let Some(file_source) = maybe_file_source {
{
let mut used_esm_sources = self.used_esm_sources.borrow_mut();
let used = used_esm_sources.get_mut(&file_source.specifier).unwrap();
*used = true;
}
let result = if let Some(load_callback) = &self.maybe_load_callback {
load_callback(file_source)
} else {

View file

@ -179,7 +179,6 @@ pub fn init_polyfill() -> Extension {
"events.ts",
"fs.ts",
"fs/promises.ts",
"global.ts",
"http.ts",
"http2.ts",
"https.ts",
@ -211,7 +210,6 @@ pub fn init_polyfill() -> Extension {
"internal_binding/uv.ts",
"internal/assert.mjs",
"internal/async_hooks.ts",
"internal/blob.mjs",
"internal/buffer.mjs",
"internal/child_process.ts",
"internal/cli_table.ts",
@ -234,7 +232,6 @@ pub fn init_polyfill() -> Extension {
"internal/crypto/random.ts",
"internal/crypto/scrypt.ts",
"internal/crypto/sig.ts",
"internal/crypto/types.ts",
"internal/crypto/util.ts",
"internal/crypto/x509.ts",
"internal/dgram.ts",
@ -245,7 +242,6 @@ pub fn init_polyfill() -> Extension {
"internal/errors.ts",
"internal/event_target.mjs",
"internal/fixed_queue.ts",
"internal/freelist.ts",
"internal/fs/streams.mjs",
"internal/fs/utils.mjs",
"internal/hide_stack_frames.ts",
@ -270,7 +266,6 @@ pub fn init_polyfill() -> Extension {
"internal/streams/duplex.mjs",
"internal/streams/end-of-stream.mjs",
"internal/streams/lazy_transform.mjs",
"internal/streams/legacy.mjs",
"internal/streams/passthrough.mjs",
"internal/streams/readable.mjs",
"internal/streams/state.mjs",
@ -287,8 +282,6 @@ pub fn init_polyfill() -> Extension {
"internal/util/types.ts",
"internal/validators.mjs",
"module_all.ts",
"module_esm.ts",
"module.js",
"net.ts",
"os.ts",
"path.ts",
@ -318,7 +311,6 @@ pub fn init_polyfill() -> Extension {
"timers/promises.ts",
"tls.ts",
"tty.ts",
"upstream_modules.ts",
"url.ts",
"util.ts",
"util/types.ts",

View file

@ -1,91 +0,0 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file no-var
import processModule from "internal:deno_node/process.ts";
import { Buffer as bufferModule } from "internal:deno_node/buffer.ts";
import {
clearInterval,
clearTimeout,
setInterval,
setTimeout,
} from "internal:deno_node/timers.ts";
import timers from "internal:deno_node/timers.ts";
type GlobalType = {
process: typeof processModule;
Buffer: typeof bufferModule;
setImmediate: typeof timers.setImmediate;
clearImmediate: typeof timers.clearImmediate;
setTimeout: typeof timers.setTimeout;
clearTimeout: typeof timers.clearTimeout;
setInterval: typeof timers.setInterval;
clearInterval: typeof timers.clearInterval;
};
declare global {
interface Window {
global: GlobalType;
}
interface globalThis {
global: GlobalType;
}
var global: GlobalType;
var process: typeof processModule;
var Buffer: typeof bufferModule;
type Buffer = bufferModule;
var setImmediate: typeof timers.setImmediate;
var clearImmediate: typeof timers.clearImmediate;
}
Object.defineProperty(globalThis, "global", {
value: new Proxy(globalThis, {
get(target, prop, receiver) {
switch (prop) {
case "setInterval":
return setInterval;
case "setTimeout":
return setTimeout;
case "clearInterval":
return clearInterval;
case "clearTimeout":
return clearTimeout;
default:
return Reflect.get(target, prop, receiver);
}
},
}),
writable: false,
enumerable: false,
configurable: true,
});
Object.defineProperty(globalThis, "process", {
value: processModule,
enumerable: false,
writable: true,
configurable: true,
});
Object.defineProperty(globalThis, "Buffer", {
value: bufferModule,
enumerable: false,
writable: true,
configurable: true,
});
Object.defineProperty(globalThis, "setImmediate", {
value: timers.setImmediate,
enumerable: true,
writable: true,
configurable: true,
});
Object.defineProperty(globalThis, "clearImmediate", {
value: timers.clearImmediate,
enumerable: true,
writable: true,
configurable: true,
});
export {};

View file

@ -1,7 +0,0 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Node's implementation checks for a symbol they put in the blob prototype
// Since the implementation of Blob is Deno's, the only option is to check the
// objects constructor
export function isBlob(object) {
return object instanceof Blob;
}

View file

@ -1,46 +0,0 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.
import { Buffer } from "internal:deno_node/buffer.ts";
export type HASH_DATA = string | ArrayBufferView | Buffer;
export type BinaryToTextEncoding = "base64" | "base64url" | "hex" | "binary";
export type CharacterEncoding = "utf8" | "utf-8" | "utf16le" | "latin1";
export type LegacyCharacterEncoding = "ascii" | "binary" | "ucs2" | "ucs-2";
export type Encoding =
| BinaryToTextEncoding
| CharacterEncoding
| LegacyCharacterEncoding;
export type ECDHKeyFormat = "compressed" | "uncompressed" | "hybrid";
export type BinaryLike = string | ArrayBufferView;
export type KeyFormat = "pem" | "der";
export type KeyType =
| "rsa"
| "rsa-pss"
| "dsa"
| "ec"
| "ed25519"
| "ed448"
| "x25519"
| "x448";
export interface PrivateKeyInput {
key: string | Buffer;
format?: KeyFormat | undefined;
type?: "pkcs1" | "pkcs8" | "sec1" | undefined;
passphrase?: string | Buffer | undefined;
}
export interface PublicKeyInput {
key: string | Buffer;
format?: KeyFormat | undefined;
type?: "pkcs1" | "spki" | undefined;
}

View file

@ -1,30 +0,0 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
type Fn<T> = (...args: unknown[]) => T;
export class FreeList<T> {
name: string;
ctor: Fn<T>;
max: number;
list: Array<T>;
constructor(name: string, max: number, ctor: Fn<T>) {
this.name = name;
this.ctor = ctor;
this.max = max;
this.list = [];
}
alloc(): T {
return this.list.length > 0
? this.list.pop()
: Reflect.apply(this.ctor, this, arguments);
}
free(obj: T) {
if (this.list.length < this.max) {
this.list.push(obj);
return true;
}
return false;
}
}

View file

@ -1,113 +0,0 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// deno-lint-ignore-file
import EE from "internal:deno_node/events.ts";
function Stream(opts) {
EE.call(this, opts);
}
Object.setPrototypeOf(Stream.prototype, EE.prototype);
Object.setPrototypeOf(Stream, EE);
Stream.prototype.pipe = function (dest, options) {
// deno-lint-ignore no-this-alias
const source = this;
function ondata(chunk) {
if (dest.writable && dest.write(chunk) === false && source.pause) {
source.pause();
}
}
source.on("data", ondata);
function ondrain() {
if (source.readable && source.resume) {
source.resume();
}
}
dest.on("drain", ondrain);
// If the 'end' option is not supplied, dest.end() will be called when
// source gets the 'end' or 'close' events. Only dest.end() once.
if (!dest._isStdio && (!options || options.end !== false)) {
source.on("end", onend);
source.on("close", onclose);
}
let didOnEnd = false;
function onend() {
if (didOnEnd) return;
didOnEnd = true;
dest.end();
}
function onclose() {
if (didOnEnd) return;
didOnEnd = true;
if (typeof dest.destroy === "function") dest.destroy();
}
// Don't leave dangling pipes when there are errors.
function onerror(er) {
cleanup();
if (EE.listenerCount(this, "error") === 0) {
this.emit("error", er);
}
}
prependListener(source, "error", onerror);
prependListener(dest, "error", onerror);
// Remove all the event listeners that were added.
function cleanup() {
source.removeListener("data", ondata);
dest.removeListener("drain", ondrain);
source.removeListener("end", onend);
source.removeListener("close", onclose);
source.removeListener("error", onerror);
dest.removeListener("error", onerror);
source.removeListener("end", cleanup);
source.removeListener("close", cleanup);
dest.removeListener("close", cleanup);
}
source.on("end", cleanup);
source.on("close", cleanup);
dest.on("close", cleanup);
dest.emit("pipe", source);
// Allow for unix-like usage: A.pipe(B).pipe(C)
return dest;
};
function prependListener(emitter, event, fn) {
// Sadly this is not cacheable as some libraries bundle their own
// event emitter implementation with them.
if (typeof emitter.prependListener === "function") {
return emitter.prependListener(event, fn);
}
// This is a hack to make sure that our error handler is attached before any
// userland ones. NEVER DO THIS. This is here only because this code needs
// to continue to work with older versions of Node.js that do not include
// the prependListener() method. The goal is to eventually remove this hack.
if (!emitter._events || !emitter._events[event]) {
emitter.on(event, fn);
} else if (Array.isArray(emitter._events[event])) {
emitter._events[event].unshift(fn);
} else {
emitter._events[event] = [fn, emitter._events[event]];
}
}
export { prependListener, Stream };

View file

@ -1,20 +0,0 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
const internals = globalThis.__bootstrap.internals;
const m = internals.require.Module;
export const _cache = m._cache;
export const _extensions = m._extensions;
export const _findPath = m._findPath;
export const _initPaths = m._initPaths;
export const _load = m._load;
export const _nodeModulePaths = m._nodeModulePaths;
export const _pathCache = m._pathCache;
export const _preloadModules = m._preloadModules;
export const _resolveFilename = m._resolveFilename;
export const _resolveLookupPaths = m._resolveLookupPaths;
export const builtinModules = m.builtinModules;
export const createRequire = m.createRequire;
export const globalPaths = m.globalPaths;
export const Module = m.Module;
export const wrap = m.wrap;
export default m;

View file

@ -1,842 +0,0 @@
// 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/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/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;
}

View file

@ -1,39 +0,0 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Upstream modules
const callerPath = `const callerCallsite = require("caller-callsite");
const re = /^file:/;
module.exports = () => {
const fileUrl = callerCallsite().getFileName();
return fileUrl.replace(re, "");
};
`;
// From: https://github.com/stefanpenner/get-caller-file/blob/2383bf9e98ed3c568ff69d7586cf59c0f1dcb9d3/index.ts
const getCallerFile = `
const re = /^file:\\/\\//;
module.exports = function getCallerFile(position = 2) {
if (position >= Error.stackTraceLimit) {
throw new TypeError('getCallerFile(position) requires position be less then Error.stackTraceLimit but position was: "' + position + '" and Error.stackTraceLimit was: "' + Error.stackTraceLimit + '"');
}
const oldPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
const stack = new Error().stack;
Error.prepareStackTrace = oldPrepareStackTrace;
if (stack !== null && typeof stack === 'object') {
// stack[0] holds this file
// stack[1] holds where this function was called
// stack[2] holds the file we're interested in
return stack[position] ? stack[position].getFileName().replace(re, "") : undefined;
}
};
`;
export default {
"caller-path": callerPath,
"get-caller-file": getCallerFile,
} as Record<string, string>;