summaryrefslogtreecommitdiff
path: root/ext/node
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node')
-rw-r--r--ext/node/01_node.js111
-rw-r--r--ext/node/02_require.js (renamed from ext/node/01_require.js)212
-rw-r--r--ext/node/Cargo.toml2
-rw-r--r--ext/node/errors.rs122
-rw-r--r--ext/node/lib.rs380
-rw-r--r--ext/node/package_json.rs159
-rw-r--r--ext/node/resolution.rs696
7 files changed, 1525 insertions, 157 deletions
diff --git a/ext/node/01_node.js b/ext/node/01_node.js
new file mode 100644
index 000000000..80fccf843
--- /dev/null
+++ b/ext/node/01_node.js
@@ -0,0 +1,111 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file
+
+"use strict";
+
+((window) => {
+ const {
+ ArrayPrototypePush,
+ ObjectEntries,
+ ObjectCreate,
+ } = window.__bootstrap.primordials;
+
+ function assert(cond) {
+ if (!cond) {
+ throw Error("assert");
+ }
+ }
+
+ let initialized = false;
+ const nodeGlobals = {};
+ const nodeGlobalThis = new Proxy(globalThis, {
+ get(_target, prop, _receiver) {
+ if (prop in nodeGlobals) {
+ return nodeGlobals[prop];
+ } else {
+ return globalThis[prop];
+ }
+ },
+ set(_target, prop, value) {
+ if (prop in nodeGlobals) {
+ nodeGlobals[prop] = value;
+ } else {
+ globalThis[prop] = value;
+ }
+ return true;
+ },
+ deleteProperty(_target, prop) {
+ let success = false;
+ if (prop in nodeGlobals) {
+ delete nodeGlobals[prop];
+ success = true;
+ }
+ if (prop in globalThis) {
+ delete globalThis[prop];
+ success = true;
+ }
+ return success;
+ },
+ ownKeys(_target) {
+ const globalThisKeys = Reflect.ownKeys(globalThis);
+ const nodeGlobalsKeys = Reflect.ownKeys(nodeGlobals);
+ const nodeGlobalsKeySet = new Set(nodeGlobalsKeys);
+ return [
+ ...ArrayPrototypeFilter(
+ globalThisKeys,
+ (k) => !nodeGlobalsKeySet.has(k),
+ ),
+ ...nodeGlobalsKeys,
+ ];
+ },
+ defineProperty(_target, prop, desc) {
+ if (prop in nodeGlobals) {
+ return Reflect.defineProperty(nodeGlobals, prop, desc);
+ } else {
+ return Reflect.defineProperty(globalThis, prop, desc);
+ }
+ },
+ getOwnPropertyDescriptor(_target, prop) {
+ if (prop in nodeGlobals) {
+ return Reflect.getOwnPropertyDescriptor(nodeGlobals, prop);
+ } else {
+ return Reflect.getOwnPropertyDescriptor(globalThis, prop);
+ }
+ },
+ has(_target, prop) {
+ return prop in nodeGlobals || prop in globalThis;
+ },
+ });
+
+ const nativeModuleExports = ObjectCreate(null);
+ const builtinModules = [];
+
+ function initialize(nodeModules) {
+ assert(!initialized);
+ initialized = true;
+ for (const [name, exports] of ObjectEntries(nodeModules)) {
+ nativeModuleExports[name] = exports;
+ ArrayPrototypePush(builtinModules, name);
+ }
+ nodeGlobals.Buffer = nativeModuleExports["buffer"].Buffer;
+ nodeGlobals.clearImmediate = nativeModuleExports["timers"].clearImmediate;
+ nodeGlobals.clearInterval = nativeModuleExports["timers"].clearInterval;
+ nodeGlobals.clearTimeout = nativeModuleExports["timers"].clearTimeout;
+ nodeGlobals.global = nodeGlobals;
+ nodeGlobals.process = nativeModuleExports["process"];
+ nodeGlobals.setImmediate = nativeModuleExports["timers"].setImmediate;
+ nodeGlobals.setInterval = nativeModuleExports["timers"].setInterval;
+ nodeGlobals.setTimeout = nativeModuleExports["timers"].setTimeout;
+ }
+
+ window.__bootstrap.internals = {
+ ...window.__bootstrap.internals ?? {},
+ node: {
+ globalThis: nodeGlobalThis,
+ initialize,
+ nativeModuleExports,
+ builtinModules,
+ },
+ };
+})(globalThis);
diff --git a/ext/node/01_require.js b/ext/node/02_require.js
index 27b71580d..1fcd0167c 100644
--- a/ext/node/01_require.js
+++ b/ext/node/02_require.js
@@ -13,9 +13,9 @@
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
+ FunctionPrototypeBind,
ObjectGetOwnPropertyDescriptor,
ObjectGetPrototypeOf,
- ObjectEntries,
ObjectPrototypeHasOwnProperty,
ObjectSetPrototypeOf,
ObjectKeys,
@@ -26,6 +26,7 @@
JSONParse,
StringPrototypeEndsWith,
StringPrototypeIndexOf,
+ StringPrototypeMatch,
StringPrototypeSlice,
StringPrototypeStartsWith,
StringPrototypeCharCodeAt,
@@ -33,6 +34,7 @@
} = window.__bootstrap.primordials;
const core = window.Deno.core;
const ops = core.ops;
+ const { node } = window.__bootstrap.internals;
// Map used to store CJS parsing data.
const cjsParseCache = new SafeWeakMap();
@@ -51,12 +53,7 @@
}
}
- // TODO(bartlomieju): verify in other parts of this file that
- // we have initialized the system before making APIs work
- let cjsInitialized = false;
- let processGlobal = null;
const nativeModulePolyfill = new SafeMap();
- const nativeModuleExports = ObjectCreate(null);
const relativeResolveCache = ObjectCreate(null);
let requireDepth = 0;
@@ -99,9 +96,7 @@
}
function tryPackage(requestPath, exts, isMain, originalPath) {
- // const pkg = readPackage(requestPath)?.main;
- let pkg = false;
-
+ const pkg = core.ops.op_require_read_package_scope(requestPath).main;
if (!pkg) {
return tryExtensions(
pathResolve(requestPath, "index"),
@@ -110,7 +105,7 @@
);
}
- const filename = path.resolve(requestPath, pkg);
+ const filename = pathResolve(requestPath, pkg);
let actual = tryFile(filename, isMain) ||
tryExtensions(filename, exts, isMain) ||
tryExtensions(
@@ -142,7 +137,7 @@
requestPath,
"package.json",
);
- process.emitWarning(
+ node.globalThis.process.emitWarning(
`Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` +
"Please either fix that or report it to the module author",
"DeprecationWarning",
@@ -214,7 +209,7 @@
}
function emitCircularRequireWarning(prop) {
- processGlobal.emitWarning(
+ node.globalThis.process.emitWarning(
`Accessing non-existent property '${String(prop)}' of module exports ` +
"inside circular dependency",
);
@@ -255,8 +250,7 @@
this.children = [];
}
- const builtinModules = [];
- Module.builtinModules = builtinModules;
+ Module.builtinModules = node.builtinModules;
Module._extensions = Object.create(null);
Module._cache = Object.create(null);
@@ -266,7 +260,54 @@
const CHAR_FORWARD_SLASH = 47;
const TRAILING_SLASH_REGEX = /(?:^|\/)\.?\.$/;
- Module._findPath = function (request, paths, isMain) {
+ const encodedSepRegEx = /%2F|%2C/i;
+
+ function finalizeEsmResolution(
+ resolved,
+ parentPath,
+ pkgPath,
+ ) {
+ if (RegExpPrototypeTest(encodedSepRegEx, resolved)) {
+ throw new ERR_INVALID_MODULE_SPECIFIER(
+ resolved,
+ 'must not include encoded "/" or "\\" characters',
+ parentPath,
+ );
+ }
+ // const filename = fileURLToPath(resolved);
+ const filename = resolved;
+ const actual = tryFile(filename, false);
+ if (actual) {
+ return actual;
+ }
+ throw new ERR_MODULE_NOT_FOUND(
+ filename,
+ path.resolve(pkgPath, "package.json"),
+ );
+ }
+
+ // This only applies to requests of a specific form:
+ // 1. name/.*
+ // 2. @scope/name/.*
+ const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
+ function resolveExports(modulesPath, request, parentPath) {
+ // The implementation's behavior is meant to mirror resolution in ESM.
+ const [, name, expansion = ""] =
+ StringPrototypeMatch(request, EXPORTS_PATTERN) || [];
+ if (!name) {
+ return;
+ }
+
+ return core.ops.op_require_resolve_exports(
+ modulesPath,
+ request,
+ name,
+ expansion,
+ parentPath,
+ ) ?? false;
+ }
+
+ Module._findPath = function (request, paths, isMain, parentPath) {
const absoluteRequest = ops.op_require_path_is_absolute(request);
if (absoluteRequest) {
paths = [""];
@@ -295,23 +336,29 @@
if (curPath && stat(curPath) < 1) continue;
if (!absoluteRequest) {
- const exportsResolved = resolveExports(curPath, request);
+ const exportsResolved = resolveExports(curPath, request, parentPath);
if (exportsResolved) {
return exportsResolved;
}
}
- const basePath = pathResolve(curPath, request);
+ const isDenoDirPackage = Deno.core.opSync(
+ "op_require_is_deno_dir_package",
+ curPath,
+ );
+ const isRelative = ops.op_require_is_request_relative(
+ request,
+ );
+ // TODO(bartlomieju): could be a single op
+ const basePath = (isDenoDirPackage && !isRelative)
+ ? pathResolve(curPath, packageSpecifierSubPath(request))
+ : pathResolve(curPath, request);
let filename;
const rc = stat(basePath);
if (!trailingSlash) {
if (rc === 0) { // File.
- if (!isMain) {
- filename = toRealPath(basePath);
- } else {
- filename = toRealPath(basePath);
- }
+ filename = toRealPath(basePath);
}
if (!filename) {
@@ -345,11 +392,23 @@
};
Module._resolveLookupPaths = function (request, parent) {
- return ops.op_require_resolve_lookup_paths(
+ const paths = [];
+ if (parent?.filename && parent.filename.length > 0) {
+ const denoDirPath = core.opSync(
+ "op_require_resolve_deno_dir",
+ request,
+ parent.filename,
+ );
+ if (denoDirPath) {
+ paths.push(denoDirPath);
+ }
+ }
+ paths.push(...ops.op_require_resolve_lookup_paths(
request,
parent?.paths,
parent?.filename ?? "",
- );
+ ));
+ return paths;
};
Module._load = function (request, parent, isMain) {
@@ -392,14 +451,9 @@
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded) {
- const parseCachedModule = cjsParseCache.get(cachedModule);
- if (!parseCachedModule || parseCachedModule.loaded) {
- return getExportsForCircularRequire(cachedModule);
- }
- parseCachedModule.loaded = true;
- } else {
- return cachedModule.exports;
+ return getExportsForCircularRequire(cachedModule);
}
+ return cachedModule.exports;
}
const mod = loadNativeModule(filename, request);
@@ -408,12 +462,11 @@
) {
return mod.exports;
}
-
// Don't call updateChildren(), Module constructor already does.
const module = cachedModule || new Module(filename, parent);
if (isMain) {
- processGlobal.mainModule = module;
+ node.globalThis.process.mainModule = module;
module.id = ".";
}
@@ -504,38 +557,23 @@
if (parent?.filename) {
if (request[0] === "#") {
- console.log("TODO: Module._resolveFilename with #specifier");
- // const pkg = readPackageScope(parent.filename) || {};
- // if (pkg.data?.imports != null) {
- // try {
- // return finalizeEsmResolution(
- // packageImportsResolve(
- // request,
- // pathToFileURL(parent.filename),
- // cjsConditions,
- // ),
- // parent.filename,
- // pkg.path,
- // );
- // } catch (e) {
- // if (e.code === "ERR_MODULE_NOT_FOUND") {
- // throw createEsmNotFoundErr(request);
- // }
- // throw e;
- // }
- // }
+ const maybeResolved = core.ops.op_require_package_imports_resolve(
+ parent.filename,
+ request,
+ );
+ if (maybeResolved) {
+ return maybeResolved;
+ }
}
}
// Try module self resolution first
- // TODO(bartlomieju): make into a single op
const parentPath = ops.op_require_try_self_parent_path(
!!parent,
parent?.filename,
parent?.id,
);
- // const selfResolved = ops.op_require_try_self(parentPath, request);
- const selfResolved = false;
+ const selfResolved = ops.op_require_try_self(parentPath, request);
if (selfResolved) {
const cacheKey = request + "\x00" +
(paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, "\x00"));
@@ -544,7 +582,12 @@
}
// Look up the filename first, since that's the cache key.
- const filename = Module._findPath(request, paths, isMain, false);
+ const filename = Module._findPath(
+ request,
+ paths,
+ isMain,
+ parentPath,
+ );
if (filename) return filename;
const requireStack = [];
for (let cursor = parent; cursor; cursor = moduleParentCache.get(cursor)) {
@@ -609,8 +652,8 @@
// TODO:
// We provide non standard timer APIs in the CommonJS wrapper
// to avoid exposing them in global namespace.
- "(function (exports, require, module, __filename, __dirname, setTimeout, clearTimeout, setInterval, clearInterval) { (function (exports, require, module, __filename, __dirname) {",
- "\n}).call(this, exports, require, module, __filename, __dirname); })",
+ "(function (exports, require, module, __filename, __dirname, globalThis) { (function (exports, require, module, __filename, __dirname, globalThis, Buffer, clearImmediate, clearInterval, clearTimeout, global, process, setImmediate, setInterval, setTimeout) {",
+ "\n}).call(this, exports, require, module, __filename, __dirname, globalThis, globalThis.Buffer, globalThis.clearImmediate, globalThis.clearInterval, globalThis.clearTimeout, globalThis.global, globalThis.process, globalThis.setImmediate, globalThis.setInterval, globalThis.setTimeout); })",
];
Module.wrap = function (script) {
script = script.replace(/^#!.*?\n/, "");
@@ -641,7 +684,7 @@
const wrapper = Module.wrap(content);
const [f, err] = core.evalContext(wrapper, filename);
if (err) {
- if (processGlobal.mainModule === cjsModuleInstance) {
+ if (node.globalThis.process.mainModule === cjsModuleInstance) {
enrichCJSError(err.thrown);
}
throw err.thrown;
@@ -667,6 +710,7 @@
this,
filename,
dirname,
+ node.globalThis,
);
if (requireDepth === 0) {
statCache = null;
@@ -677,7 +721,14 @@
Module._extensions[".js"] = function (module, filename) {
const content = ops.op_require_read_file(filename);
- console.log(`TODO: Module._extensions[".js"] is ESM`);
+ if (StringPrototypeEndsWith(filename, ".js")) {
+ const pkg = core.ops.op_require_read_package_scope(filename);
+ if (pkg && pkg.exists && pkg.typ == "module") {
+ throw new Error(
+ `Import ESM module: ${filename} from ${module.parent.filename}`,
+ );
+ }
+ }
module._compile(content, filename);
};
@@ -738,8 +789,9 @@
return require;
}
- function createRequire(filename) {
+ function createRequire(filenameOrUrl) {
// FIXME: handle URLs and validation
+ const filename = core.opSync("op_require_as_file_path", filenameOrUrl);
return createRequireFromPath(filename);
}
@@ -775,13 +827,13 @@
wrap: Module.wrap,
};
- nativeModuleExports.module = m;
+ node.nativeModuleExports.module = m;
function loadNativeModule(_id, request) {
if (nativeModulePolyfill.has(request)) {
return nativeModulePolyfill.get(request);
}
- const modExports = nativeModuleExports[request];
+ const modExports = node.nativeModuleExports[request];
if (modExports) {
const nodeMod = new Module(request);
nodeMod.exports = modExports;
@@ -793,21 +845,31 @@
}
function nativeModuleCanBeRequiredByUsers(request) {
- return !!nativeModuleExports[request];
+ return !!node.nativeModuleExports[request];
+ }
+
+ function readPackageScope() {
+ throw new Error("not implemented");
}
- function initializeCommonJs(nodeModules, process) {
- assert(!cjsInitialized);
- cjsInitialized = true;
- for (const [name, exports] of ObjectEntries(nodeModules)) {
- nativeModuleExports[name] = exports;
- ArrayPrototypePush(Module.builtinModules, name);
+ function bindExport(value, mod) {
+ // ensure exported functions are bound to their module object
+ if (typeof value === "function") {
+ return FunctionPrototypeBind(value, mod);
+ } else {
+ return value;
}
- processGlobal = process;
}
- function readPackageScope() {
- throw new Error("not implemented");
+ /** @param specifier {string} */
+ function packageSpecifierSubPath(specifier) {
+ let parts = specifier.split("/");
+ if (parts[0].startsWith("@")) {
+ parts = parts.slice(2);
+ } else {
+ parts = parts.slice(1);
+ }
+ return parts.join("/");
}
window.__bootstrap.internals = {
@@ -818,7 +880,7 @@
toRealPath,
cjsParseCache,
readPackageScope,
- initializeCommonJs,
+ bindExport,
},
};
})(globalThis);
diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml
index c6975ee15..026b1fef4 100644
--- a/ext/node/Cargo.toml
+++ b/ext/node/Cargo.toml
@@ -15,3 +15,5 @@ path = "lib.rs"
[dependencies]
deno_core = { version = "0.147.0", path = "../../core" }
+regex = "1"
+serde = "1.0.136"
diff --git a/ext/node/errors.rs b/ext/node/errors.rs
new file mode 100644
index 000000000..8d1822f7b
--- /dev/null
+++ b/ext/node/errors.rs
@@ -0,0 +1,122 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::error::generic_error;
+use deno_core::error::type_error;
+use deno_core::error::AnyError;
+
+pub fn err_invalid_module_specifier(
+ request: &str,
+ reason: &str,
+ maybe_base: Option<String>,
+) -> AnyError {
+ let mut msg = format!(
+ "[ERR_INVALID_MODULE_SPECIFIER] Invalid module \"{}\" {}",
+ request, reason
+ );
+
+ if let Some(base) = maybe_base {
+ msg = format!("{} imported from {}", msg, base);
+ }
+
+ type_error(msg)
+}
+
+#[allow(unused)]
+pub fn err_invalid_package_config(
+ path: &str,
+ maybe_base: Option<String>,
+ maybe_message: Option<String>,
+) -> AnyError {
+ let mut msg = format!(
+ "[ERR_INVALID_PACKAGE_CONFIG] Invalid package config {}",
+ path
+ );
+
+ if let Some(base) = maybe_base {
+ msg = format!("{} while importing {}", msg, base);
+ }
+
+ if let Some(message) = maybe_message {
+ msg = format!("{}. {}", msg, message);
+ }
+
+ generic_error(msg)
+}
+
+#[allow(unused)]
+pub fn err_module_not_found(path: &str, base: &str, typ: &str) -> AnyError {
+ generic_error(format!(
+ "[ERR_MODULE_NOT_FOUND] Cannot find {} \"{}\" imported from \"{}\"",
+ typ, path, base
+ ))
+}
+
+pub fn err_invalid_package_target(
+ pkg_path: String,
+ key: String,
+ target: String,
+ is_import: bool,
+ maybe_base: Option<String>,
+) -> AnyError {
+ let rel_error = !is_import && !target.is_empty() && !target.starts_with("./");
+ let mut msg = "[ERR_INVALID_PACKAGE_TARGET]".to_string();
+
+ if key == "." {
+ assert!(!is_import);
+ msg = format!("{} Invalid \"exports\" main target {} defined in the package config {}package.json", msg, target, pkg_path)
+ } else {
+ let ie = if is_import { "imports" } else { "exports" };
+ msg = format!("{} Invalid \"{}\" target {} defined for '{}' in the package config {}package.json", msg, ie, target, key, pkg_path)
+ };
+
+ if let Some(base) = maybe_base {
+ msg = format!("{} imported from {}", msg, base);
+ };
+ if rel_error {
+ msg = format!("{}; target must start with \"./\"", msg);
+ }
+
+ generic_error(msg)
+}
+
+pub fn err_package_path_not_exported(
+ pkg_path: String,
+ subpath: String,
+ maybe_base: Option<String>,
+) -> AnyError {
+ let mut msg = "[ERR_PACKAGE_PATH_NOT_EXPORTED]".to_string();
+
+ if subpath == "." {
+ msg = format!(
+ "{} No \"exports\" main defined in {}package.json",
+ msg, pkg_path
+ );
+ } else {
+ msg = format!("{} Package subpath \'{}\' is not defined by \"exports\" in {}package.json", msg, subpath, pkg_path);
+ };
+
+ if let Some(base) = maybe_base {
+ msg = format!("{} imported from {}", msg, base);
+ }
+
+ generic_error(msg)
+}
+
+pub fn err_package_import_not_defined(
+ specifier: &str,
+ package_path: Option<String>,
+ base: &str,
+) -> AnyError {
+ let mut msg = format!(
+ "[ERR_PACKAGE_IMPORT_NOT_DEFINED] Package import specifier \"{}\" is not defined in",
+ specifier
+ );
+
+ if let Some(package_path) = package_path {
+ msg = format!("{} in package {}package.json", msg, package_path);
+ }
+
+ msg = format!("{} imported from {}", msg, base);
+
+ type_error(msg)
+}
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index c61e27079..6be376e6d 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -4,22 +4,60 @@ use deno_core::error::AnyError;
use deno_core::include_js_files;
use deno_core::normalize_path;
use deno_core::op;
+use deno_core::url::Url;
use deno_core::Extension;
use deno_core::OpState;
+use std::path::Path;
use std::path::PathBuf;
+use std::rc::Rc;
+
+pub use package_json::PackageJson;
+pub use resolution::get_package_scope_config;
+pub use resolution::legacy_main_resolve;
+pub use resolution::package_exports_resolve;
+pub use resolution::package_imports_resolve;
+pub use resolution::package_resolve;
+pub use resolution::DEFAULT_CONDITIONS;
+
+pub trait DenoDirNpmResolver {
+ fn resolve_package_folder_from_package(
+ &self,
+ specifier: &str,
+ referrer: &Path,
+ ) -> Result<PathBuf, AnyError>;
+
+ fn resolve_package_folder_from_path(
+ &self,
+ path: &Path,
+ ) -> Result<PathBuf, AnyError>;
+
+ fn in_npm_package(&self, path: &Path) -> bool;
+
+ fn ensure_read_permission(&self, path: &Path) -> Result<(), AnyError>;
+}
+
+mod errors;
+mod package_json;
+mod resolution;
-pub struct Unstable(pub bool);
+struct Unstable(pub bool);
-pub fn init(unstable: bool) -> Extension {
+pub fn init(
+ unstable: bool,
+ maybe_npm_resolver: Option<Rc<dyn DenoDirNpmResolver>>,
+) -> Extension {
Extension::builder()
.js(include_js_files!(
prefix "deno:ext/node",
- "01_require.js",
+ "01_node.js",
+ "02_require.js",
))
.ops(vec![
op_require_init_paths::decl(),
op_require_node_module_paths::decl(),
op_require_proxy_path::decl(),
+ op_require_is_deno_dir_package::decl(),
+ op_require_resolve_deno_dir::decl(),
op_require_is_request_relative::decl(),
op_require_resolve_lookup_paths::decl(),
op_require_try_self_parent_path::decl(),
@@ -31,9 +69,16 @@ pub fn init(unstable: bool) -> Extension {
op_require_path_resolve::decl(),
op_require_path_basename::decl(),
op_require_read_file::decl(),
+ op_require_as_file_path::decl(),
+ op_require_resolve_exports::decl(),
+ op_require_read_package_scope::decl(),
+ op_require_package_imports_resolve::decl(),
])
.state(move |state| {
state.put(Unstable(unstable));
+ if let Some(npm_resolver) = maybe_npm_resolver.clone() {
+ state.put(npm_resolver);
+ }
Ok(())
})
.build()
@@ -48,60 +93,74 @@ fn check_unstable(state: &OpState) {
}
}
+fn ensure_read_permission(
+ state: &mut OpState,
+ file_path: &Path,
+) -> Result<(), AnyError> {
+ let resolver = {
+ let resolver = state.borrow::<Rc<dyn DenoDirNpmResolver>>();
+ resolver.clone()
+ };
+ resolver.ensure_read_permission(file_path)
+}
+
#[op]
pub fn op_require_init_paths(state: &mut OpState) -> Vec<String> {
check_unstable(state);
- let (home_dir, node_path) = if cfg!(windows) {
- (
- std::env::var("USERPROFILE").unwrap_or_else(|_| "".into()),
- std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()),
- )
- } else {
- (
- std::env::var("HOME").unwrap_or_else(|_| "".into()),
- std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()),
- )
- };
-
- let mut prefix_dir = std::env::current_exe().unwrap();
- if cfg!(windows) {
- prefix_dir = prefix_dir.join("..").join("..")
- } else {
- prefix_dir = prefix_dir.join("..")
- }
+ // todo(dsherret): this code is node compat mode specific and
+ // we probably don't want it for small mammal, so ignore it for now
+
+ // let (home_dir, node_path) = if cfg!(windows) {
+ // (
+ // std::env::var("USERPROFILE").unwrap_or_else(|_| "".into()),
+ // std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()),
+ // )
+ // } else {
+ // (
+ // std::env::var("HOME").unwrap_or_else(|_| "".into()),
+ // std::env::var("NODE_PATH").unwrap_or_else(|_| "".into()),
+ // )
+ // };
+
+ // let mut prefix_dir = std::env::current_exe().unwrap();
+ // if cfg!(windows) {
+ // prefix_dir = prefix_dir.join("..").join("..")
+ // } else {
+ // prefix_dir = prefix_dir.join("..")
+ // }
- let mut paths = vec![prefix_dir.join("lib").join("node")];
+ // let mut paths = vec![prefix_dir.join("lib").join("node")];
- if !home_dir.is_empty() {
- paths.insert(0, PathBuf::from(&home_dir).join(".node_libraries"));
- paths.insert(0, PathBuf::from(&home_dir).join(".nod_modules"));
- }
+ // if !home_dir.is_empty() {
+ // paths.insert(0, PathBuf::from(&home_dir).join(".node_libraries"));
+ // paths.insert(0, PathBuf::from(&home_dir).join(".nod_modules"));
+ // }
- let mut paths = paths
- .into_iter()
- .map(|p| p.to_string_lossy().to_string())
- .collect();
-
- if !node_path.is_empty() {
- let delimiter = if cfg!(windows) { ";" } else { ":" };
- let mut node_paths: Vec<String> = node_path
- .split(delimiter)
- .filter(|e| !e.is_empty())
- .map(|s| s.to_string())
- .collect();
- node_paths.append(&mut paths);
- paths = node_paths;
- }
+ // let mut paths = paths
+ // .into_iter()
+ // .map(|p| p.to_string_lossy().to_string())
+ // .collect();
+
+ // if !node_path.is_empty() {
+ // let delimiter = if cfg!(windows) { ";" } else { ":" };
+ // let mut node_paths: Vec<String> = node_path
+ // .split(delimiter)
+ // .filter(|e| !e.is_empty())
+ // .map(|s| s.to_string())
+ // .collect();
+ // node_paths.append(&mut paths);
+ // paths = node_paths;
+ // }
- paths
+ vec![]
}
#[op]
pub fn op_require_node_module_paths(
state: &mut OpState,
from: String,
-) -> Vec<String> {
+) -> Result<Vec<String>, AnyError> {
check_unstable(state);
// Guarantee that "from" is absolute.
let from = deno_core::resolve_path(&from)
@@ -109,6 +168,8 @@ pub fn op_require_node_module_paths(
.to_file_path()
.unwrap();
+ ensure_read_permission(state, &from)?;
+
if cfg!(windows) {
// return root node_modules when path is 'D:\\'.
let from_str = from.to_str().unwrap();
@@ -117,14 +178,14 @@ pub fn op_require_node_module_paths(
if bytes[from_str.len() - 1] == b'\\' && bytes[from_str.len() - 2] == b':'
{
let p = from_str.to_owned() + "node_modules";
- return vec![p];
+ return Ok(vec![p]);
}
}
} else {
// Return early not only to avoid unnecessary work, but to *avoid* returning
// an array of two items for a root: [ '//node_modules', '/node_modules' ]
if from.to_string_lossy() == "/" {
- return vec!["/node_modules".to_string()];
+ return Ok(vec!["/node_modules".to_string()]);
}
}
@@ -144,7 +205,7 @@ pub fn op_require_node_module_paths(
paths.push("/node_modules".to_string());
}
- paths
+ Ok(paths)
}
#[op]
@@ -171,11 +232,8 @@ fn op_require_is_request_relative(
request: String,
) -> bool {
check_unstable(state);
- if request.starts_with("./") {
- return true;
- }
-
- if request.starts_with("../") {
+ if request.starts_with("./") || request.starts_with("../") || request == ".."
+ {
return true;
}
@@ -193,6 +251,30 @@ fn op_require_is_request_relative(
}
#[op]
+fn op_require_resolve_deno_dir(
+ state: &mut OpState,
+ request: String,
+ parent_filename: String,
+) -> Option<String> {
+ check_unstable(state);
+ let resolver = state.borrow::<Rc<dyn DenoDirNpmResolver>>();
+ resolver
+ .resolve_package_folder_from_package(
+ &request,
+ &PathBuf::from(parent_filename),
+ )
+ .ok()
+ .map(|p| p.to_string_lossy().to_string())
+}
+
+#[op]
+fn op_require_is_deno_dir_package(state: &mut OpState, path: String) -> bool {
+ check_unstable(state);
+ let resolver = state.borrow::<Rc<dyn DenoDirNpmResolver>>();
+ resolver.in_npm_package(&PathBuf::from(path))
+}
+
+#[op]
fn op_require_resolve_lookup_paths(
state: &mut OpState,
request: String,
@@ -242,17 +324,19 @@ fn op_require_path_is_absolute(state: &mut OpState, p: String) -> bool {
}
#[op]
-fn op_require_stat(state: &mut OpState, filename: String) -> i32 {
+fn op_require_stat(state: &mut OpState, path: String) -> Result<i32, AnyError> {
check_unstable(state);
- if let Ok(metadata) = std::fs::metadata(&filename) {
+ let path = PathBuf::from(path);
+ ensure_read_permission(state, &path)?;
+ if let Ok(metadata) = std::fs::metadata(&path) {
if metadata.is_file() {
- return 0;
+ return Ok(0);
} else {
- return 1;
+ return Ok(1);
}
}
- -1
+ Ok(-1)
}
#[op]
@@ -261,7 +345,9 @@ fn op_require_real_path(
request: String,
) -> Result<String, AnyError> {
check_unstable(state);
- let mut canonicalized_path = PathBuf::from(request).canonicalize()?;
+ let path = PathBuf::from(request);
+ ensure_read_permission(state, &path)?;
+ let mut canonicalized_path = path.canonicalize()?;
if cfg!(windows) {
canonicalized_path = PathBuf::from(
canonicalized_path
@@ -273,9 +359,7 @@ fn op_require_real_path(
Ok(canonicalized_path.to_string_lossy().to_string())
}
-#[op]
-fn op_require_path_resolve(state: &mut OpState, parts: Vec<String>) -> String {
- check_unstable(state);
+fn path_resolve(parts: Vec<String>) -> String {
assert!(!parts.is_empty());
let mut p = PathBuf::from(&parts[0]);
if parts.len() > 1 {
@@ -287,6 +371,12 @@ fn op_require_path_resolve(state: &mut OpState, parts: Vec<String>) -> String {
}
#[op]
+fn op_require_path_resolve(state: &mut OpState, parts: Vec<String>) -> String {
+ check_unstable(state);
+ path_resolve(parts)
+}
+
+#[op]
fn op_require_path_dirname(state: &mut OpState, request: String) -> String {
check_unstable(state);
let p = PathBuf::from(request);
@@ -306,57 +396,183 @@ fn op_require_try_self_parent_path(
has_parent: bool,
maybe_parent_filename: Option<String>,
maybe_parent_id: Option<String>,
-) -> Option<String> {
+) -> Result<Option<String>, AnyError> {
check_unstable(state);
if !has_parent {
- return None;
+ return Ok(None);
}
if let Some(parent_filename) = maybe_parent_filename {
- return Some(parent_filename);
+ return Ok(Some(parent_filename));
}
if let Some(parent_id) = maybe_parent_id {
if parent_id == "<repl>" || parent_id == "internal/preload" {
if let Ok(cwd) = std::env::current_dir() {
- return Some(cwd.to_string_lossy().to_string());
+ ensure_read_permission(state, &cwd)?;
+ return Ok(Some(cwd.to_string_lossy().to_string()));
}
}
}
- None
+ Ok(None)
}
#[op]
fn op_require_try_self(
state: &mut OpState,
- has_parent: bool,
- maybe_parent_filename: Option<String>,
- maybe_parent_id: Option<String>,
-) -> Option<String> {
+ parent_path: Option<String>,
+ request: String,
+) -> Result<Option<String>, AnyError> {
check_unstable(state);
- if !has_parent {
- return None;
+ if parent_path.is_none() {
+ return Ok(None);
}
- if let Some(parent_filename) = maybe_parent_filename {
- return Some(parent_filename);
+ let resolver = state.borrow::<Rc<dyn DenoDirNpmResolver>>().clone();
+ let pkg = resolution::get_package_scope_config(
+ &Url::from_file_path(parent_path.unwrap()).unwrap(),
+ &*resolver,
+ )
+ .ok();
+ if pkg.is_none() {
+ return Ok(None);
}
- if let Some(parent_id) = maybe_parent_id {
- if parent_id == "<repl>" || parent_id == "internal/preload" {
- if let Ok(cwd) = std::env::current_dir() {
- return Some(cwd.to_string_lossy().to_string());
- }
- }
+ let pkg = pkg.unwrap();
+ if pkg.exports.is_none() {
+ return Ok(None);
+ }
+ if pkg.name.is_none() {
+ return Ok(None);
+ }
+
+ let pkg_name = pkg.name.as_ref().unwrap().to_string();
+ let mut expansion = ".".to_string();
+
+ if request == pkg_name {
+ // pass
+ } else if request.starts_with(&format!("{}/", pkg_name)) {
+ expansion += &request[pkg_name.len()..];
+ } else {
+ return Ok(None);
+ }
+
+ let base = deno_core::url::Url::from_file_path(PathBuf::from("/")).unwrap();
+ if let Some(exports) = &pkg.exports {
+ resolution::package_exports_resolve(
+ deno_core::url::Url::from_file_path(&pkg.path).unwrap(),
+ expansion,
+ exports,
+ &base,
+ resolution::REQUIRE_CONDITIONS,
+ &*resolver,
+ )
+ .map(|r| Some(r.as_str().to_string()))
+ } else {
+ Ok(None)
}
- None
}
#[op]
fn op_require_read_file(
state: &mut OpState,
- _filename: String,
+ file_path: String,
) -> Result<String, AnyError> {
check_unstable(state);
- todo!("not implemented");
+ let file_path = PathBuf::from(file_path);
+ ensure_read_permission(state, &file_path)?;
+ Ok(std::fs::read_to_string(file_path)?)
+}
+
+#[op]
+pub fn op_require_as_file_path(
+ state: &mut OpState,
+ file_or_url: String,
+) -> String {
+ check_unstable(state);
+ match Url::parse(&file_or_url) {
+ Ok(url) => url.to_file_path().unwrap().to_string_lossy().to_string(),
+ Err(_) => file_or_url,
+ }
+}
+
+#[op]
+fn op_require_resolve_exports(
+ state: &mut OpState,
+ modules_path: String,
+ _request: String,
+ name: String,
+ expansion: String,
+ parent_path: String,
+) -> Result<Option<String>, AnyError> {
+ check_unstable(state);
+ let resolver = state.borrow::<Rc<dyn DenoDirNpmResolver>>().clone();
+
+ let pkg_path = if resolver.in_npm_package(&PathBuf::from(&modules_path)) {
+ modules_path
+ } else {
+ path_resolve(vec![modules_path, name])
+ };
+ let pkg = PackageJson::load(
+ &*resolver,
+ PathBuf::from(&pkg_path).join("package.json"),
+ )?;
+
+ if let Some(exports) = &pkg.exports {
+ let base = Url::from_file_path(parent_path).unwrap();
+ resolution::package_exports_resolve(
+ deno_core::url::Url::from_directory_path(pkg_path).unwrap(),
+ format!(".{}", expansion),
+ exports,
+ &base,
+ resolution::REQUIRE_CONDITIONS,
+ &*resolver,
+ )
+ .map(|r| Some(r.to_file_path().unwrap().to_string_lossy().to_string()))
+ } else {
+ Ok(None)
+ }
+}
+
+#[op]
+fn op_require_read_package_scope(
+ state: &mut OpState,
+ filename: String,
+) -> Option<PackageJson> {
+ check_unstable(state);
+ let resolver = state.borrow::<Rc<dyn DenoDirNpmResolver>>().clone();
+ resolution::get_package_scope_config(
+ &Url::from_file_path(filename).unwrap(),
+ &*resolver,
+ )
+ .ok()
+}
+
+#[op]
+fn op_require_package_imports_resolve(
+ state: &mut OpState,
+ parent_filename: String,
+ request: String,
+) -> Result<Option<String>, AnyError> {
+ check_unstable(state);
+ let parent_path = PathBuf::from(&parent_filename);
+ ensure_read_permission(state, &parent_path)?;
+ let resolver = state.borrow::<Rc<dyn DenoDirNpmResolver>>().clone();
+ let pkg = PackageJson::load(&*resolver, parent_path.join("package.json"))?;
+
+ if pkg.imports.is_some() {
+ let referrer =
+ deno_core::url::Url::from_file_path(&parent_filename).unwrap();
+ let r = resolution::package_imports_resolve(
+ &request,
+ &referrer,
+ resolution::REQUIRE_CONDITIONS,
+ &*resolver,
+ )
+ .map(|r| Some(r.as_str().to_string()));
+ state.put(resolver);
+ r
+ } else {
+ Ok(None)
+ }
}
diff --git a/ext/node/package_json.rs b/ext/node/package_json.rs
new file mode 100644
index 000000000..19a79da96
--- /dev/null
+++ b/ext/node/package_json.rs
@@ -0,0 +1,159 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use super::DenoDirNpmResolver;
+use deno_core::anyhow;
+use deno_core::anyhow::bail;
+use deno_core::error::AnyError;
+use deno_core::serde_json;
+use deno_core::serde_json::Map;
+use deno_core::serde_json::Value;
+use serde::Serialize;
+use std::io::ErrorKind;
+use std::path::PathBuf;
+
+// TODO(bartlomieju): deduplicate with cli/compat/esm_resolver.rs
+#[derive(Clone, Debug, Serialize)]
+pub struct PackageJson {
+ pub exists: bool,
+ pub exports: Option<Map<String, Value>>,
+ pub imports: Option<Map<String, Value>>,
+ pub bin: Option<Value>,
+ pub main: Option<String>,
+ pub name: Option<String>,
+ pub path: PathBuf,
+ pub typ: String,
+ pub types: Option<String>,
+}
+
+impl PackageJson {
+ pub fn empty(path: PathBuf) -> PackageJson {
+ PackageJson {
+ exists: false,
+ exports: None,
+ imports: None,
+ bin: None,
+ main: None,
+ name: None,
+ path,
+ typ: "none".to_string(),
+ types: None,
+ }
+ }
+
+ pub fn load(
+ resolver: &dyn DenoDirNpmResolver,
+ path: PathBuf,
+ ) -> Result<PackageJson, AnyError> {
+ resolver.ensure_read_permission(&path)?;
+ let source = match std::fs::read_to_string(&path) {
+ Ok(source) => source,
+ Err(err) if err.kind() == ErrorKind::NotFound => {
+ return Ok(PackageJson::empty(path));
+ }
+ Err(err) => bail!(
+ "Error loading package.json at {}. {:#}",
+ path.display(),
+ err
+ ),
+ };
+
+ if source.trim().is_empty() {
+ return Ok(PackageJson::empty(path));
+ }
+
+ let package_json: Value = serde_json::from_str(&source)
+ .map_err(|err| anyhow::anyhow!("malformed package.json {}", err))?;
+
+ let imports_val = package_json.get("imports");
+ let main_val = package_json.get("main");
+ let name_val = package_json.get("name");
+ let type_val = package_json.get("type");
+ let bin = package_json.get("bin").map(ToOwned::to_owned);
+ let exports = package_json.get("exports").map(|exports| {
+ if is_conditional_exports_main_sugar(exports) {
+ let mut map = Map::new();
+ map.insert(".".to_string(), exports.to_owned());
+ map
+ } else {
+ exports.as_object().unwrap().to_owned()
+ }
+ });
+
+ let imports = if let Some(imp) = imports_val {
+ imp.as_object().map(|imp| imp.to_owned())
+ } else {
+ None
+ };
+ let main = if let Some(m) = main_val {
+ m.as_str().map(|m| m.to_string())
+ } else {
+ None
+ };
+ let name = if let Some(n) = name_val {
+ n.as_str().map(|n| n.to_string())
+ } else {
+ None
+ };
+
+ // Ignore unknown types for forwards compatibility
+ let typ = if let Some(t) = type_val {
+ if let Some(t) = t.as_str() {
+ if t != "module" && t != "commonjs" {
+ "none".to_string()
+ } else {
+ t.to_string()
+ }
+ } else {
+ "none".to_string()
+ }
+ } else {
+ "none".to_string()
+ };
+
+ // for typescript, it looks for "typings" first, then "types"
+ let types = package_json
+ .get("typings")
+ .or_else(|| package_json.get("types"))
+ .and_then(|t| t.as_str().map(|s| s.to_string()));
+
+ let package_json = PackageJson {
+ exists: true,
+ path,
+ main,
+ name,
+ typ,
+ types,
+ exports,
+ imports,
+ bin,
+ };
+ Ok(package_json)
+ }
+}
+
+fn is_conditional_exports_main_sugar(exports: &Value) -> bool {
+ if exports.is_string() || exports.is_array() {
+ return true;
+ }
+
+ if exports.is_null() || !exports.is_object() {
+ return false;
+ }
+
+ let exports_obj = exports.as_object().unwrap();
+ let mut is_conditional_sugar = false;
+ let mut i = 0;
+ for key in exports_obj.keys() {
+ let cur_is_conditional_sugar = key.is_empty() || !key.starts_with('.');
+ if i == 0 {
+ is_conditional_sugar = cur_is_conditional_sugar;
+ i += 1;
+ } else if is_conditional_sugar != cur_is_conditional_sugar {
+ panic!("\"exports\" cannot contains 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.")
+ }
+ }
+
+ is_conditional_sugar
+}
diff --git a/ext/node/resolution.rs b/ext/node/resolution.rs
new file mode 100644
index 000000000..9d71fba49
--- /dev/null
+++ b/ext/node/resolution.rs
@@ -0,0 +1,696 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::path::PathBuf;
+
+use deno_core::error::generic_error;
+use deno_core::error::AnyError;
+use deno_core::serde_json::Map;
+use deno_core::serde_json::Value;
+use deno_core::url::Url;
+use deno_core::ModuleSpecifier;
+use regex::Regex;
+
+use crate::errors;
+use crate::package_json::PackageJson;
+use crate::DenoDirNpmResolver;
+
+pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"];
+pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"];
+
+fn to_file_path(url: &ModuleSpecifier) -> PathBuf {
+ url
+ .to_file_path()
+ .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {}", url))
+}
+
+fn to_file_path_string(url: &ModuleSpecifier) -> String {
+ to_file_path(url).display().to_string()
+}
+
+fn throw_import_not_defined(
+ specifier: &str,
+ package_json_url: Option<ModuleSpecifier>,
+ base: &ModuleSpecifier,
+) -> AnyError {
+ errors::err_package_import_not_defined(
+ specifier,
+ package_json_url.map(|u| to_file_path_string(&u.join(".").unwrap())),
+ &to_file_path_string(base),
+ )
+}
+
+fn pattern_key_compare(a: &str, b: &str) -> i32 {
+ let a_pattern_index = a.find('*');
+ let b_pattern_index = b.find('*');
+
+ let base_len_a = if let Some(index) = a_pattern_index {
+ index + 1
+ } else {
+ a.len()
+ };
+ let base_len_b = if let Some(index) = b_pattern_index {
+ index + 1
+ } else {
+ b.len()
+ };
+
+ if base_len_a > base_len_b {
+ return -1;
+ }
+
+ if base_len_b > base_len_a {
+ return 1;
+ }
+
+ if a_pattern_index.is_none() {
+ return 1;
+ }
+
+ if b_pattern_index.is_none() {
+ return -1;
+ }
+
+ if a.len() > b.len() {
+ return -1;
+ }
+
+ if b.len() > a.len() {
+ return 1;
+ }
+
+ 0
+}
+
+pub fn package_imports_resolve(
+ name: &str,
+ referrer: &ModuleSpecifier,
+ conditions: &[&str],
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<ModuleSpecifier, AnyError> {
+ if name == "#" || name.starts_with("#/") || name.ends_with('/') {
+ let reason = "is not a valid internal imports specifier name";
+ return Err(errors::err_invalid_module_specifier(
+ name,
+ reason,
+ Some(to_file_path_string(referrer)),
+ ));
+ }
+
+ let package_config = get_package_scope_config(referrer, npm_resolver)?;
+ let mut package_json_url = None;
+ if package_config.exists {
+ package_json_url = Some(Url::from_file_path(package_config.path).unwrap());
+ if let Some(imports) = &package_config.imports {
+ if imports.contains_key(name) && !name.contains('*') {
+ let maybe_resolved = resolve_package_target(
+ package_json_url.clone().unwrap(),
+ imports.get(name).unwrap().to_owned(),
+ "".to_string(),
+ name.to_string(),
+ referrer,
+ false,
+ true,
+ conditions,
+ npm_resolver,
+ )?;
+ if let Some(resolved) = maybe_resolved {
+ return Ok(resolved);
+ }
+ } else {
+ let mut best_match = "";
+ let mut best_match_subpath = None;
+ for key in imports.keys() {
+ let pattern_index = key.find('*');
+ if let Some(pattern_index) = pattern_index {
+ let key_sub = &key[0..=pattern_index];
+ if name.starts_with(key_sub) {
+ let pattern_trailer = &key[pattern_index + 1..];
+ if name.len() > key.len()
+ && name.ends_with(&pattern_trailer)
+ && pattern_key_compare(best_match, key) == 1
+ && key.rfind('*') == Some(pattern_index)
+ {
+ best_match = key;
+ best_match_subpath = Some(
+ name[pattern_index..=(name.len() - pattern_trailer.len())]
+ .to_string(),
+ );
+ }
+ }
+ }
+ }
+
+ if !best_match.is_empty() {
+ let target = imports.get(best_match).unwrap().to_owned();
+ let maybe_resolved = resolve_package_target(
+ package_json_url.clone().unwrap(),
+ target,
+ best_match_subpath.unwrap(),
+ best_match.to_string(),
+ referrer,
+ true,
+ true,
+ conditions,
+ npm_resolver,
+ )?;
+ if let Some(resolved) = maybe_resolved {
+ return Ok(resolved);
+ }
+ }
+ }
+ }
+ }
+
+ Err(throw_import_not_defined(name, package_json_url, referrer))
+}
+
+fn throw_invalid_package_target(
+ subpath: String,
+ target: String,
+ package_json_url: &ModuleSpecifier,
+ internal: bool,
+ base: &ModuleSpecifier,
+) -> AnyError {
+ errors::err_invalid_package_target(
+ to_file_path_string(&package_json_url.join(".").unwrap()),
+ subpath,
+ target,
+ internal,
+ Some(base.as_str().to_string()),
+ )
+}
+
+fn throw_invalid_subpath(
+ subpath: String,
+ package_json_url: &ModuleSpecifier,
+ internal: bool,
+ base: &ModuleSpecifier,
+) -> AnyError {
+ let ie = if internal { "imports" } else { "exports" };
+ let reason = format!(
+ "request is not a valid subpath for the \"{}\" resolution of {}",
+ ie,
+ to_file_path_string(package_json_url)
+ );
+ errors::err_invalid_module_specifier(
+ &subpath,
+ &reason,
+ Some(to_file_path_string(base)),
+ )
+}
+
+#[allow(clippy::too_many_arguments)]
+fn resolve_package_target_string(
+ target: String,
+ subpath: String,
+ match_: String,
+ package_json_url: ModuleSpecifier,
+ base: &ModuleSpecifier,
+ pattern: bool,
+ internal: bool,
+ conditions: &[&str],
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<ModuleSpecifier, AnyError> {
+ if !subpath.is_empty() && !pattern && !target.ends_with('/') {
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ &package_json_url,
+ internal,
+ base,
+ ));
+ }
+ let invalid_segment_re =
+ Regex::new(r"(^|\|/)(..?|node_modules)(\|/|$)").expect("bad regex");
+ let pattern_re = Regex::new(r"\*").expect("bad regex");
+ if !target.starts_with("./") {
+ if internal && !target.starts_with("../") && !target.starts_with('/') {
+ let is_url = Url::parse(&target).is_ok();
+ if !is_url {
+ let export_target = if pattern {
+ pattern_re
+ .replace(&target, |_caps: &regex::Captures| subpath.clone())
+ .to_string()
+ } else {
+ format!("{}{}", target, subpath)
+ };
+ return package_resolve(
+ &export_target,
+ &package_json_url,
+ conditions,
+ npm_resolver,
+ );
+ }
+ }
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ &package_json_url,
+ internal,
+ base,
+ ));
+ }
+ if invalid_segment_re.is_match(&target[2..]) {
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ &package_json_url,
+ internal,
+ base,
+ ));
+ }
+ let resolved = package_json_url.join(&target)?;
+ let resolved_path = resolved.path();
+ let package_url = package_json_url.join(".").unwrap();
+ let package_path = package_url.path();
+ if !resolved_path.starts_with(package_path) {
+ return Err(throw_invalid_package_target(
+ match_,
+ target,
+ &package_json_url,
+ internal,
+ base,
+ ));
+ }
+ if subpath.is_empty() {
+ return Ok(resolved);
+ }
+ if invalid_segment_re.is_match(&subpath) {
+ let request = if pattern {
+ match_.replace('*', &subpath)
+ } else {
+ format!("{}{}", match_, subpath)
+ };
+ return Err(throw_invalid_subpath(
+ request,
+ &package_json_url,
+ internal,
+ base,
+ ));
+ }
+ if pattern {
+ let replaced = pattern_re
+ .replace(resolved.as_str(), |_caps: &regex::Captures| subpath.clone());
+ let url = Url::parse(&replaced)?;
+ return Ok(url);
+ }
+ Ok(resolved.join(&subpath)?)
+}
+
+#[allow(clippy::too_many_arguments)]
+fn resolve_package_target(
+ package_json_url: ModuleSpecifier,
+ target: Value,
+ subpath: String,
+ package_subpath: String,
+ base: &ModuleSpecifier,
+ pattern: bool,
+ internal: bool,
+ conditions: &[&str],
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<Option<ModuleSpecifier>, AnyError> {
+ if let Some(target) = target.as_str() {
+ return Ok(Some(resolve_package_target_string(
+ target.to_string(),
+ subpath,
+ package_subpath,
+ package_json_url,
+ base,
+ pattern,
+ internal,
+ conditions,
+ npm_resolver,
+ )?));
+ } else if let Some(target_arr) = target.as_array() {
+ if target_arr.is_empty() {
+ return Ok(None);
+ }
+
+ let mut last_error = None;
+ for target_item in target_arr {
+ let resolved_result = resolve_package_target(
+ package_json_url.clone(),
+ target_item.to_owned(),
+ subpath.clone(),
+ package_subpath.clone(),
+ base,
+ pattern,
+ internal,
+ conditions,
+ npm_resolver,
+ );
+
+ if let Err(e) = resolved_result {
+ let err_string = e.to_string();
+ last_error = Some(e);
+ if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") {
+ continue;
+ }
+ return Err(last_error.unwrap());
+ }
+ let resolved = resolved_result.unwrap();
+ if resolved.is_none() {
+ last_error = None;
+ continue;
+ }
+ return Ok(resolved);
+ }
+ if last_error.is_none() {
+ return Ok(None);
+ }
+ return Err(last_error.unwrap());
+ } else if let Some(target_obj) = target.as_object() {
+ for key in target_obj.keys() {
+ // TODO(bartlomieju): verify that keys are not numeric
+ // return Err(errors::err_invalid_package_config(
+ // to_file_path_string(package_json_url),
+ // Some(base.as_str().to_string()),
+ // Some("\"exports\" cannot contain numeric property keys.".to_string()),
+ // ));
+
+ if key == "default" || conditions.contains(&key.as_str()) {
+ let condition_target = target_obj.get(key).unwrap().to_owned();
+ let resolved = resolve_package_target(
+ package_json_url.clone(),
+ condition_target,
+ subpath.clone(),
+ package_subpath.clone(),
+ base,
+ pattern,
+ internal,
+ conditions,
+ npm_resolver,
+ )?;
+ if resolved.is_none() {
+ continue;
+ }
+ return Ok(resolved);
+ }
+ }
+ } else if target.is_null() {
+ return Ok(None);
+ }
+
+ Err(throw_invalid_package_target(
+ package_subpath,
+ target.to_string(),
+ &package_json_url,
+ internal,
+ base,
+ ))
+}
+
+fn throw_exports_not_found(
+ subpath: String,
+ package_json_url: &ModuleSpecifier,
+ base: &ModuleSpecifier,
+) -> AnyError {
+ errors::err_package_path_not_exported(
+ to_file_path_string(&package_json_url.join(".").unwrap()),
+ subpath,
+ Some(to_file_path_string(base)),
+ )
+}
+
+pub fn package_exports_resolve(
+ package_json_url: ModuleSpecifier,
+ package_subpath: String,
+ package_exports: &Map<String, Value>,
+ base: &ModuleSpecifier,
+ conditions: &[&str],
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<ModuleSpecifier, AnyError> {
+ if package_exports.contains_key(&package_subpath)
+ && package_subpath.find('*').is_none()
+ && !package_subpath.ends_with('/')
+ {
+ let target = package_exports.get(&package_subpath).unwrap().to_owned();
+ let resolved = resolve_package_target(
+ package_json_url.clone(),
+ target,
+ "".to_string(),
+ package_subpath.to_string(),
+ base,
+ false,
+ false,
+ conditions,
+ npm_resolver,
+ )?;
+ if resolved.is_none() {
+ return Err(throw_exports_not_found(
+ package_subpath,
+ &package_json_url,
+ base,
+ ));
+ }
+ return Ok(resolved.unwrap());
+ }
+
+ let mut best_match = "";
+ let mut best_match_subpath = None;
+ for key in package_exports.keys() {
+ let pattern_index = key.find('*');
+ if let Some(pattern_index) = pattern_index {
+ let key_sub = &key[0..=pattern_index];
+ if package_subpath.starts_with(key_sub) {
+ // 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 package_subpath.ends_with('/') {
+ // TODO(bartlomieju):
+ // emitTrailingSlashPatternDeprecation();
+ }
+ let pattern_trailer = &key[pattern_index + 1..];
+ if package_subpath.len() > key.len()
+ && package_subpath.ends_with(&pattern_trailer)
+ && pattern_key_compare(best_match, key) == 1
+ && key.rfind('*') == Some(pattern_index)
+ {
+ best_match = key;
+ best_match_subpath = Some(
+ package_subpath
+ [pattern_index..=(package_subpath.len() - pattern_trailer.len())]
+ .to_string(),
+ );
+ }
+ }
+ }
+ }
+
+ if !best_match.is_empty() {
+ let target = package_exports.get(best_match).unwrap().to_owned();
+ let maybe_resolved = resolve_package_target(
+ package_json_url.clone(),
+ target,
+ best_match_subpath.unwrap(),
+ best_match.to_string(),
+ base,
+ true,
+ false,
+ conditions,
+ npm_resolver,
+ )?;
+ if let Some(resolved) = maybe_resolved {
+ return Ok(resolved);
+ } else {
+ return Err(throw_exports_not_found(
+ package_subpath,
+ &package_json_url,
+ base,
+ ));
+ }
+ }
+
+ Err(throw_exports_not_found(
+ package_subpath,
+ &package_json_url,
+ base,
+ ))
+}
+
+fn parse_package_name(
+ specifier: &str,
+ base: &ModuleSpecifier,
+) -> Result<(String, String, bool), AnyError> {
+ let mut separator_index = specifier.find('/');
+ let mut valid_package_name = true;
+ let mut is_scoped = false;
+ if specifier.is_empty() {
+ valid_package_name = false;
+ } else if specifier.starts_with('@') {
+ is_scoped = true;
+ if let Some(index) = separator_index {
+ separator_index = specifier[index + 1..].find('/');
+ } else {
+ valid_package_name = false;
+ }
+ }
+
+ let package_name = if let Some(index) = separator_index {
+ specifier[0..index].to_string()
+ } else {
+ specifier.to_string()
+ };
+
+ // Package name cannot have leading . and cannot have percent-encoding or separators.
+ for ch in package_name.chars() {
+ if ch == '%' || ch == '\\' {
+ valid_package_name = false;
+ break;
+ }
+ }
+
+ if !valid_package_name {
+ return Err(errors::err_invalid_module_specifier(
+ specifier,
+ "is not a valid package name",
+ Some(to_file_path_string(base)),
+ ));
+ }
+
+ let package_subpath = if let Some(index) = separator_index {
+ format!(".{}", specifier.chars().skip(index).collect::<String>())
+ } else {
+ ".".to_string()
+ };
+
+ Ok((package_name, package_subpath, is_scoped))
+}
+
+pub fn package_resolve(
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ conditions: &[&str],
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<ModuleSpecifier, AnyError> {
+ let (package_name, package_subpath, _is_scoped) =
+ parse_package_name(specifier, referrer)?;
+
+ // ResolveSelf
+ let package_config = get_package_scope_config(referrer, npm_resolver)?;
+ if package_config.exists {
+ let package_json_url = Url::from_file_path(&package_config.path).unwrap();
+ if package_config.name.as_ref() == Some(&package_name) {
+ if let Some(exports) = &package_config.exports {
+ return package_exports_resolve(
+ package_json_url,
+ package_subpath,
+ exports,
+ referrer,
+ conditions,
+ npm_resolver,
+ );
+ }
+ }
+ }
+
+ let package_dir_path = npm_resolver
+ .resolve_package_folder_from_package(
+ &package_name,
+ &referrer.to_file_path().unwrap(),
+ )
+ .unwrap();
+ let package_json_path = package_dir_path.join("package.json");
+ let package_json_url =
+ ModuleSpecifier::from_file_path(&package_json_path).unwrap();
+
+ // todo: error with this instead when can't find package
+ // Err(errors::err_module_not_found(
+ // &package_json_url
+ // .join(".")
+ // .unwrap()
+ // .to_file_path()
+ // .unwrap()
+ // .display()
+ // .to_string(),
+ // &to_file_path_string(referrer),
+ // "package",
+ // ))
+
+ // Package match.
+ let package_json = PackageJson::load(npm_resolver, package_json_path)?;
+ if let Some(exports) = &package_json.exports {
+ return package_exports_resolve(
+ package_json_url,
+ package_subpath,
+ exports,
+ referrer,
+ conditions,
+ npm_resolver,
+ );
+ }
+ if package_subpath == "." {
+ return legacy_main_resolve(&package_json_url, &package_json, referrer);
+ }
+
+ package_json_url
+ .join(&package_subpath)
+ .map_err(AnyError::from)
+}
+
+pub fn get_package_scope_config(
+ referrer: &ModuleSpecifier,
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<PackageJson, AnyError> {
+ let root_folder = npm_resolver
+ .resolve_package_folder_from_path(&referrer.to_file_path().unwrap())?;
+ let package_json_path = root_folder.join("./package.json");
+ PackageJson::load(npm_resolver, package_json_path)
+}
+
+fn file_exists(path_url: &ModuleSpecifier) -> bool {
+ if let Ok(stats) = std::fs::metadata(to_file_path(path_url)) {
+ stats.is_file()
+ } else {
+ false
+ }
+}
+
+pub fn legacy_main_resolve(
+ package_json_url: &ModuleSpecifier,
+ package_json: &PackageJson,
+ _base: &ModuleSpecifier,
+) -> Result<ModuleSpecifier, AnyError> {
+ let mut guess;
+
+ if let Some(main) = &package_json.main {
+ guess = package_json_url.join(&format!("./{}", main))?;
+ if file_exists(&guess) {
+ return Ok(guess);
+ }
+
+ let mut found = false;
+ for ext in [
+ ".js",
+ ".json",
+ ".node",
+ "/index.js",
+ "/index.json",
+ "/index.node",
+ ] {
+ guess = package_json_url.join(&format!("./{}{}", main, ext))?;
+ if file_exists(&guess) {
+ found = true;
+ break;
+ }
+ }
+
+ if found {
+ // TODO(bartlomieju): emitLegacyIndexDeprecation()
+ return Ok(guess);
+ }
+ }
+
+ for p in ["./index.js", "./index.json", "./index.node"] {
+ guess = package_json_url.join(p)?;
+ if file_exists(&guess) {
+ // TODO(bartlomieju): emitLegacyIndexDeprecation()
+ return Ok(guess);
+ }
+ }
+
+ Err(generic_error("not found"))
+}