mirror of
https://github.com/denoland/deno.git
synced 2024-12-04 17:18:23 -05:00
1327 lines
35 KiB
Rust
1327 lines
35 KiB
Rust
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use std::collections::HashSet;
|
|
use std::collections::VecDeque;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::cache::NodeAnalysisCache;
|
|
use crate::deno_std::CURRENT_STD_URL;
|
|
use deno_ast::CjsAnalysis;
|
|
use deno_ast::MediaType;
|
|
use deno_ast::ModuleSpecifier;
|
|
use deno_core::anyhow::anyhow;
|
|
use deno_core::anyhow::bail;
|
|
use deno_core::anyhow::Context;
|
|
use deno_core::error::generic_error;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::located_script_name;
|
|
use deno_core::serde_json::Value;
|
|
use deno_core::url::Url;
|
|
use deno_core::JsRuntime;
|
|
use deno_runtime::deno_node::errors;
|
|
use deno_runtime::deno_node::get_closest_package_json;
|
|
use deno_runtime::deno_node::legacy_main_resolve;
|
|
use deno_runtime::deno_node::package_exports_resolve;
|
|
use deno_runtime::deno_node::package_imports_resolve;
|
|
use deno_runtime::deno_node::package_resolve;
|
|
use deno_runtime::deno_node::path_to_declaration_path;
|
|
use deno_runtime::deno_node::NodeModuleKind;
|
|
use deno_runtime::deno_node::PackageJson;
|
|
use deno_runtime::deno_node::PathClean;
|
|
use deno_runtime::deno_node::RequireNpmResolver;
|
|
use deno_runtime::deno_node::DEFAULT_CONDITIONS;
|
|
use deno_runtime::deno_node::NODE_GLOBAL_THIS_NAME;
|
|
use deno_runtime::deno_node::TYPES_CONDITIONS;
|
|
use once_cell::sync::Lazy;
|
|
use regex::Regex;
|
|
|
|
use crate::file_fetcher::FileFetcher;
|
|
use crate::npm::NpmPackageReference;
|
|
use crate::npm::NpmPackageReq;
|
|
use crate::npm::NpmPackageResolver;
|
|
|
|
mod analyze;
|
|
|
|
pub use analyze::esm_code_with_node_globals;
|
|
|
|
#[derive(Debug)]
|
|
pub enum NodeResolution {
|
|
Esm(ModuleSpecifier),
|
|
CommonJs(ModuleSpecifier),
|
|
BuiltIn(String),
|
|
}
|
|
|
|
impl NodeResolution {
|
|
pub fn into_url(self) -> ModuleSpecifier {
|
|
match self {
|
|
Self::Esm(u) => u,
|
|
Self::CommonJs(u) => u,
|
|
Self::BuiltIn(specifier) => {
|
|
if specifier.starts_with("node:") {
|
|
ModuleSpecifier::parse(&specifier).unwrap()
|
|
} else {
|
|
ModuleSpecifier::parse(&format!("node:{}", specifier)).unwrap()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn into_specifier_and_media_type(
|
|
resolution: Option<Self>,
|
|
) -> (ModuleSpecifier, MediaType) {
|
|
match resolution {
|
|
Some(NodeResolution::CommonJs(specifier)) => {
|
|
let media_type = MediaType::from(&specifier);
|
|
(
|
|
specifier,
|
|
match media_type {
|
|
MediaType::JavaScript | MediaType::Jsx => MediaType::Cjs,
|
|
MediaType::TypeScript | MediaType::Tsx => MediaType::Cts,
|
|
MediaType::Dts => MediaType::Dcts,
|
|
_ => media_type,
|
|
},
|
|
)
|
|
}
|
|
Some(NodeResolution::Esm(specifier)) => {
|
|
let media_type = MediaType::from(&specifier);
|
|
(
|
|
specifier,
|
|
match media_type {
|
|
MediaType::JavaScript | MediaType::Jsx => MediaType::Mjs,
|
|
MediaType::TypeScript | MediaType::Tsx => MediaType::Mts,
|
|
MediaType::Dts => MediaType::Dmts,
|
|
_ => media_type,
|
|
},
|
|
)
|
|
}
|
|
maybe_response => {
|
|
let specifier = match maybe_response {
|
|
Some(response) => response.into_url(),
|
|
None => {
|
|
ModuleSpecifier::parse("deno:///missing_dependency.d.ts").unwrap()
|
|
}
|
|
};
|
|
(specifier, MediaType::Dts)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum NodeResolutionMode {
|
|
Execution,
|
|
Types,
|
|
}
|
|
|
|
struct NodeModulePolyfill {
|
|
/// Name of the module like "assert" or "timers/promises"
|
|
name: &'static str,
|
|
|
|
/// Specifier relative to the root of `deno_std` repo, like "node/asser.ts"
|
|
specifier: &'static str,
|
|
}
|
|
|
|
static SUPPORTED_MODULES: &[NodeModulePolyfill] = &[
|
|
NodeModulePolyfill {
|
|
name: "assert",
|
|
specifier: "node/assert.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "assert/strict",
|
|
specifier: "node/assert/strict.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "async_hooks",
|
|
specifier: "node/async_hooks.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "buffer",
|
|
specifier: "node/buffer.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "child_process",
|
|
specifier: "node/child_process.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "cluster",
|
|
specifier: "node/cluster.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "console",
|
|
specifier: "node/console.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "constants",
|
|
specifier: "node/constants.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "crypto",
|
|
specifier: "node/crypto.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "dgram",
|
|
specifier: "node/dgram.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "dns",
|
|
specifier: "node/dns.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "dns/promises",
|
|
specifier: "node/dns/promises.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "domain",
|
|
specifier: "node/domain.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "events",
|
|
specifier: "node/events.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "fs",
|
|
specifier: "node/fs.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "fs/promises",
|
|
specifier: "node/fs/promises.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "http",
|
|
specifier: "node/http.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "https",
|
|
specifier: "node/https.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "module",
|
|
// NOTE(bartlomieju): `module` is special, because we don't want to use
|
|
// `deno_std/node/module.ts`, but instead use a special shim that we
|
|
// provide in `ext/node`.
|
|
specifier: "[USE `deno_node::MODULE_ES_SHIM` to get this module]",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "net",
|
|
specifier: "node/net.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "os",
|
|
specifier: "node/os.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "path",
|
|
specifier: "node/path.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "path/posix",
|
|
specifier: "node/path/posix.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "path/win32",
|
|
specifier: "node/path/win32.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "perf_hooks",
|
|
specifier: "node/perf_hooks.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "process",
|
|
specifier: "node/process.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "querystring",
|
|
specifier: "node/querystring.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "readline",
|
|
specifier: "node/readline.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "stream",
|
|
specifier: "node/stream.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "stream/consumers",
|
|
specifier: "node/stream/consumers.mjs",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "stream/promises",
|
|
specifier: "node/stream/promises.mjs",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "stream/web",
|
|
specifier: "node/stream/web.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "string_decoder",
|
|
specifier: "node/string_decoder.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "sys",
|
|
specifier: "node/sys.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "timers",
|
|
specifier: "node/timers.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "timers/promises",
|
|
specifier: "node/timers/promises.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "tls",
|
|
specifier: "node/tls.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "tty",
|
|
specifier: "node/tty.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "url",
|
|
specifier: "node/url.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "util",
|
|
specifier: "node/util.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "util/types",
|
|
specifier: "node/util/types.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "v8",
|
|
specifier: "node/v8.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "vm",
|
|
specifier: "node/vm.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "worker_threads",
|
|
specifier: "node/worker_threads.ts",
|
|
},
|
|
NodeModulePolyfill {
|
|
name: "zlib",
|
|
specifier: "node/zlib.ts",
|
|
},
|
|
];
|
|
|
|
static NODE_COMPAT_URL: Lazy<Url> = Lazy::new(|| {
|
|
if let Ok(url_str) = std::env::var("DENO_NODE_COMPAT_URL") {
|
|
let url = Url::parse(&url_str).expect(
|
|
"Malformed DENO_NODE_COMPAT_URL value, make sure it's a file URL ending with a slash"
|
|
);
|
|
return url;
|
|
}
|
|
|
|
CURRENT_STD_URL.clone()
|
|
});
|
|
|
|
pub static MODULE_ALL_URL: Lazy<Url> =
|
|
Lazy::new(|| NODE_COMPAT_URL.join("node/module_all.ts").unwrap());
|
|
|
|
fn find_builtin_node_module(specifier: &str) -> Option<&NodeModulePolyfill> {
|
|
SUPPORTED_MODULES.iter().find(|m| m.name == specifier)
|
|
}
|
|
|
|
fn is_builtin_node_module(specifier: &str) -> bool {
|
|
find_builtin_node_module(specifier).is_some()
|
|
}
|
|
|
|
pub fn resolve_builtin_node_module(specifier: &str) -> Result<Url, AnyError> {
|
|
// NOTE(bartlomieju): `module` is special, because we don't want to use
|
|
// `deno_std/node/module.ts`, but instead use a special shim that we
|
|
// provide in `ext/node`.
|
|
if specifier == "module" {
|
|
return Ok(Url::parse("node:module").unwrap());
|
|
}
|
|
|
|
if let Some(module) = find_builtin_node_module(specifier) {
|
|
let module_url = NODE_COMPAT_URL.join(module.specifier).unwrap();
|
|
return Ok(module_url);
|
|
}
|
|
|
|
Err(generic_error(format!(
|
|
"Unknown built-in Node module: {}",
|
|
specifier
|
|
)))
|
|
}
|
|
|
|
static RESERVED_WORDS: Lazy<HashSet<&str>> = Lazy::new(|| {
|
|
HashSet::from([
|
|
"break",
|
|
"case",
|
|
"catch",
|
|
"class",
|
|
"const",
|
|
"continue",
|
|
"debugger",
|
|
"default",
|
|
"delete",
|
|
"do",
|
|
"else",
|
|
"export",
|
|
"extends",
|
|
"false",
|
|
"finally",
|
|
"for",
|
|
"function",
|
|
"if",
|
|
"import",
|
|
"in",
|
|
"instanceof",
|
|
"new",
|
|
"null",
|
|
"return",
|
|
"super",
|
|
"switch",
|
|
"this",
|
|
"throw",
|
|
"true",
|
|
"try",
|
|
"typeof",
|
|
"var",
|
|
"void",
|
|
"while",
|
|
"with",
|
|
"yield",
|
|
"let",
|
|
"enum",
|
|
"implements",
|
|
"interface",
|
|
"package",
|
|
"private",
|
|
"protected",
|
|
"public",
|
|
"static",
|
|
])
|
|
});
|
|
|
|
pub async fn initialize_runtime(
|
|
js_runtime: &mut JsRuntime,
|
|
) -> Result<(), AnyError> {
|
|
let source_code = &format!(
|
|
r#"(async function loadBuiltinNodeModules(moduleAllUrl, nodeGlobalThisName) {{
|
|
const moduleAll = await import(moduleAllUrl);
|
|
Deno[Deno.internal].node.initialize(moduleAll.default, nodeGlobalThisName);
|
|
}})('{}', '{}');"#,
|
|
MODULE_ALL_URL.as_str(),
|
|
NODE_GLOBAL_THIS_NAME.as_str(),
|
|
);
|
|
|
|
let value =
|
|
js_runtime.execute_script(&located_script_name!(), source_code)?;
|
|
js_runtime.resolve_value(value).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn initialize_binary_command(
|
|
js_runtime: &mut JsRuntime,
|
|
binary_name: &str,
|
|
) -> Result<(), AnyError> {
|
|
// overwrite what's done in deno_std in order to set the binary arg name
|
|
let source_code = &format!(
|
|
r#"(async function initializeBinaryCommand(binaryName) {{
|
|
const process = Deno[Deno.internal].node.globalThis.process;
|
|
Object.defineProperty(process.argv, "0", {{
|
|
get: () => binaryName,
|
|
}});
|
|
}})('{}');"#,
|
|
binary_name,
|
|
);
|
|
|
|
let value =
|
|
js_runtime.execute_script(&located_script_name!(), source_code)?;
|
|
js_runtime.resolve_value(value).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// This function is an implementation of `defaultResolve` in
|
|
/// `lib/internal/modules/esm/resolve.js` from Node.
|
|
pub fn node_resolve(
|
|
specifier: &str,
|
|
referrer: &ModuleSpecifier,
|
|
mode: NodeResolutionMode,
|
|
npm_resolver: &dyn RequireNpmResolver,
|
|
) -> Result<Option<NodeResolution>, AnyError> {
|
|
// Note: if we are here, then the referrer is an esm module
|
|
// TODO(bartlomieju): skipped "policy" part as we don't plan to support it
|
|
|
|
if is_builtin_node_module(specifier) {
|
|
return Ok(Some(NodeResolution::BuiltIn(specifier.to_string())));
|
|
}
|
|
|
|
if let Ok(url) = Url::parse(specifier) {
|
|
if url.scheme() == "data" {
|
|
return Ok(Some(NodeResolution::Esm(url)));
|
|
}
|
|
|
|
let protocol = url.scheme();
|
|
|
|
if protocol == "node" {
|
|
let split_specifier = url.as_str().split(':');
|
|
let specifier = split_specifier.skip(1).collect::<String>();
|
|
|
|
if is_builtin_node_module(&specifier) {
|
|
return Ok(Some(NodeResolution::BuiltIn(specifier)));
|
|
}
|
|
}
|
|
|
|
if protocol != "file" && protocol != "data" {
|
|
return Err(errors::err_unsupported_esm_url_scheme(&url));
|
|
}
|
|
|
|
// todo(dsherret): this seems wrong
|
|
if referrer.scheme() == "data" {
|
|
let url = referrer.join(specifier).map_err(AnyError::from)?;
|
|
return Ok(Some(NodeResolution::Esm(url)));
|
|
}
|
|
}
|
|
|
|
let conditions = mode_conditions(mode);
|
|
let url = module_resolve(specifier, referrer, conditions, npm_resolver)?;
|
|
let url = match url {
|
|
Some(url) => url,
|
|
None => return Ok(None),
|
|
};
|
|
let url = match mode {
|
|
NodeResolutionMode::Execution => url,
|
|
NodeResolutionMode::Types => {
|
|
let path = url.to_file_path().unwrap();
|
|
// todo(16370): the module kind is not correct here. I think we need
|
|
// typescript to tell us if the referrer is esm or cjs
|
|
let path = path_to_declaration_path(path, NodeModuleKind::Esm);
|
|
ModuleSpecifier::from_file_path(path).unwrap()
|
|
}
|
|
};
|
|
|
|
let resolve_response = url_to_node_resolution(url, npm_resolver)?;
|
|
// TODO(bartlomieju): skipped checking errors for commonJS resolution and
|
|
// "preserveSymlinksMain"/"preserveSymlinks" options.
|
|
Ok(Some(resolve_response))
|
|
}
|
|
|
|
pub fn node_resolve_npm_reference(
|
|
reference: &NpmPackageReference,
|
|
mode: NodeResolutionMode,
|
|
npm_resolver: &NpmPackageResolver,
|
|
) -> Result<Option<NodeResolution>, AnyError> {
|
|
let package_folder =
|
|
npm_resolver.resolve_package_folder_from_deno_module(&reference.req)?;
|
|
let node_module_kind = NodeModuleKind::Esm;
|
|
let maybe_resolved_path = package_config_resolve(
|
|
&reference
|
|
.sub_path
|
|
.as_ref()
|
|
.map(|s| format!("./{}", s))
|
|
.unwrap_or_else(|| ".".to_string()),
|
|
&package_folder,
|
|
node_module_kind,
|
|
mode_conditions(mode),
|
|
npm_resolver,
|
|
)
|
|
.with_context(|| {
|
|
format!("Error resolving package config for '{}'.", reference)
|
|
})?;
|
|
let resolved_path = match maybe_resolved_path {
|
|
Some(resolved_path) => resolved_path,
|
|
None => return Ok(None),
|
|
};
|
|
let resolved_path = match mode {
|
|
NodeResolutionMode::Execution => resolved_path,
|
|
NodeResolutionMode::Types => {
|
|
path_to_declaration_path(resolved_path, node_module_kind)
|
|
}
|
|
};
|
|
let url = ModuleSpecifier::from_file_path(resolved_path).unwrap();
|
|
let resolve_response = url_to_node_resolution(url, npm_resolver)?;
|
|
// TODO(bartlomieju): skipped checking errors for commonJS resolution and
|
|
// "preserveSymlinksMain"/"preserveSymlinks" options.
|
|
Ok(Some(resolve_response))
|
|
}
|
|
|
|
fn mode_conditions(mode: NodeResolutionMode) -> &'static [&'static str] {
|
|
match mode {
|
|
NodeResolutionMode::Execution => DEFAULT_CONDITIONS,
|
|
NodeResolutionMode::Types => TYPES_CONDITIONS,
|
|
}
|
|
}
|
|
|
|
pub fn node_resolve_binary_export(
|
|
pkg_req: &NpmPackageReq,
|
|
bin_name: Option<&str>,
|
|
npm_resolver: &NpmPackageResolver,
|
|
) -> Result<NodeResolution, AnyError> {
|
|
let package_folder =
|
|
npm_resolver.resolve_package_folder_from_deno_module(pkg_req)?;
|
|
let package_json_path = package_folder.join("package.json");
|
|
let package_json = PackageJson::load(npm_resolver, package_json_path)?;
|
|
let bin = match &package_json.bin {
|
|
Some(bin) => bin,
|
|
None => bail!(
|
|
"package '{}' did not have a bin property in its package.json",
|
|
&pkg_req.name,
|
|
),
|
|
};
|
|
let bin_entry = resolve_bin_entry_value(pkg_req, bin_name, bin)?;
|
|
let url =
|
|
ModuleSpecifier::from_file_path(package_folder.join(bin_entry)).unwrap();
|
|
|
|
let resolve_response = url_to_node_resolution(url, npm_resolver)?;
|
|
// TODO(bartlomieju): skipped checking errors for commonJS resolution and
|
|
// "preserveSymlinksMain"/"preserveSymlinks" options.
|
|
Ok(resolve_response)
|
|
}
|
|
|
|
fn resolve_bin_entry_value<'a>(
|
|
pkg_req: &NpmPackageReq,
|
|
bin_name: Option<&str>,
|
|
bin: &'a Value,
|
|
) -> Result<&'a str, AnyError> {
|
|
let bin_entry = match bin {
|
|
Value::String(_) => {
|
|
if bin_name.is_some() && bin_name.unwrap() != pkg_req.name {
|
|
None
|
|
} else {
|
|
Some(bin)
|
|
}
|
|
}
|
|
Value::Object(o) => {
|
|
if let Some(bin_name) = bin_name {
|
|
o.get(bin_name)
|
|
} else if o.len() == 1 || o.len() > 1 && o.values().all(|v| v == o.values().next().unwrap()) {
|
|
o.values().next()
|
|
} else {
|
|
o.get(&pkg_req.name)
|
|
}
|
|
},
|
|
_ => bail!("package '{}' did not have a bin property with a string or object value in its package.json", pkg_req.name),
|
|
};
|
|
let bin_entry = match bin_entry {
|
|
Some(e) => e,
|
|
None => {
|
|
let keys = bin
|
|
.as_object()
|
|
.map(|o| {
|
|
o.keys()
|
|
.into_iter()
|
|
.map(|k| format!(" * npm:{}/{}", pkg_req, k))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
bail!(
|
|
"package '{}' did not have a bin entry for '{}' in its package.json{}",
|
|
pkg_req.name,
|
|
bin_name.unwrap_or(&pkg_req.name),
|
|
if keys.is_empty() {
|
|
"".to_string()
|
|
} else {
|
|
format!("\n\nPossibilities:\n{}", keys.join("\n"))
|
|
}
|
|
)
|
|
}
|
|
};
|
|
match bin_entry {
|
|
Value::String(s) => Ok(s),
|
|
_ => bail!(
|
|
"package '{}' had a non-string sub property of bin in its package.json",
|
|
pkg_req.name,
|
|
),
|
|
}
|
|
}
|
|
|
|
pub fn load_cjs_module_from_ext_node(
|
|
js_runtime: &mut JsRuntime,
|
|
module: &str,
|
|
main: bool,
|
|
) -> Result<(), AnyError> {
|
|
fn escape_for_single_quote_string(text: &str) -> String {
|
|
text.replace('\\', r"\\").replace('\'', r"\'")
|
|
}
|
|
|
|
let source_code = &format!(
|
|
r#"(function loadCjsModule(module) {{
|
|
Deno[Deno.internal].require.Module._load(module, null, {main});
|
|
}})('{module}');"#,
|
|
main = main,
|
|
module = escape_for_single_quote_string(module),
|
|
);
|
|
|
|
js_runtime.execute_script(&located_script_name!(), source_code)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn package_config_resolve(
|
|
package_subpath: &str,
|
|
package_dir: &Path,
|
|
referrer_kind: NodeModuleKind,
|
|
conditions: &[&str],
|
|
npm_resolver: &dyn RequireNpmResolver,
|
|
) -> Result<Option<PathBuf>, AnyError> {
|
|
let package_json_path = package_dir.join("package.json");
|
|
let referrer = ModuleSpecifier::from_directory_path(package_dir).unwrap();
|
|
let package_config =
|
|
PackageJson::load(npm_resolver, package_json_path.clone())?;
|
|
if let Some(exports) = &package_config.exports {
|
|
let is_types = conditions == TYPES_CONDITIONS;
|
|
if is_types && package_subpath == "." {
|
|
if let Ok(Some(path)) =
|
|
legacy_main_resolve(&package_config, referrer_kind, conditions)
|
|
{
|
|
return Ok(Some(path));
|
|
} else {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
return package_exports_resolve(
|
|
&package_json_path,
|
|
package_subpath.to_string(),
|
|
exports,
|
|
&referrer,
|
|
referrer_kind,
|
|
conditions,
|
|
npm_resolver,
|
|
)
|
|
.map(Some);
|
|
}
|
|
if package_subpath == "." {
|
|
return legacy_main_resolve(&package_config, referrer_kind, conditions);
|
|
}
|
|
|
|
Ok(Some(package_dir.join(package_subpath)))
|
|
}
|
|
|
|
pub fn url_to_node_resolution(
|
|
url: ModuleSpecifier,
|
|
npm_resolver: &dyn RequireNpmResolver,
|
|
) -> Result<NodeResolution, AnyError> {
|
|
let url_str = url.as_str().to_lowercase();
|
|
Ok(if url_str.starts_with("http") {
|
|
NodeResolution::Esm(url)
|
|
} else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") {
|
|
let package_config = get_closest_package_json(&url, npm_resolver)?;
|
|
if package_config.typ == "module" {
|
|
NodeResolution::Esm(url)
|
|
} else {
|
|
NodeResolution::CommonJs(url)
|
|
}
|
|
} else if url_str.ends_with(".mjs") || url_str.ends_with(".d.mts") {
|
|
NodeResolution::Esm(url)
|
|
} else {
|
|
NodeResolution::CommonJs(url)
|
|
})
|
|
}
|
|
|
|
fn finalize_resolution(
|
|
resolved: ModuleSpecifier,
|
|
base: &ModuleSpecifier,
|
|
) -> Result<ModuleSpecifier, AnyError> {
|
|
// todo(dsherret): cache
|
|
let encoded_sep_re = Regex::new(r"%2F|%2C").unwrap();
|
|
|
|
if encoded_sep_re.is_match(resolved.path()) {
|
|
return Err(errors::err_invalid_module_specifier(
|
|
resolved.path(),
|
|
"must not include encoded \"/\" or \"\\\\\" characters",
|
|
Some(to_file_path_string(base)),
|
|
));
|
|
}
|
|
|
|
let path = to_file_path(&resolved);
|
|
|
|
// TODO(bartlomieju): currently not supported
|
|
// if (getOptionValue('--experimental-specifier-resolution') === 'node') {
|
|
// ...
|
|
// }
|
|
|
|
let p_str = path.to_str().unwrap();
|
|
let p = if p_str.ends_with('/') {
|
|
p_str[p_str.len() - 1..].to_string()
|
|
} else {
|
|
p_str.to_string()
|
|
};
|
|
|
|
let (is_dir, is_file) = if let Ok(stats) = std::fs::metadata(&p) {
|
|
(stats.is_dir(), stats.is_file())
|
|
} else {
|
|
(false, false)
|
|
};
|
|
if is_dir {
|
|
return Err(errors::err_unsupported_dir_import(
|
|
resolved.as_str(),
|
|
base.as_str(),
|
|
));
|
|
} else if !is_file {
|
|
return Err(errors::err_module_not_found(
|
|
resolved.as_str(),
|
|
base.as_str(),
|
|
"module",
|
|
));
|
|
}
|
|
|
|
Ok(resolved)
|
|
}
|
|
|
|
fn module_resolve(
|
|
specifier: &str,
|
|
referrer: &ModuleSpecifier,
|
|
conditions: &[&str],
|
|
npm_resolver: &dyn RequireNpmResolver,
|
|
) -> Result<Option<ModuleSpecifier>, AnyError> {
|
|
// note: if we're here, the referrer is an esm module
|
|
let url = if should_be_treated_as_relative_or_absolute_path(specifier) {
|
|
let resolved_specifier = referrer.join(specifier)?;
|
|
if conditions == TYPES_CONDITIONS {
|
|
let file_path = to_file_path(&resolved_specifier);
|
|
// todo(dsherret): the node module kind is not correct and we
|
|
// should use the value provided by typescript instead
|
|
let declaration_path =
|
|
path_to_declaration_path(file_path, NodeModuleKind::Esm);
|
|
Some(ModuleSpecifier::from_file_path(declaration_path).unwrap())
|
|
} else {
|
|
Some(resolved_specifier)
|
|
}
|
|
} else if specifier.starts_with('#') {
|
|
Some(
|
|
package_imports_resolve(
|
|
specifier,
|
|
referrer,
|
|
NodeModuleKind::Esm,
|
|
conditions,
|
|
npm_resolver,
|
|
)
|
|
.map(|p| ModuleSpecifier::from_file_path(p).unwrap())?,
|
|
)
|
|
} else if let Ok(resolved) = Url::parse(specifier) {
|
|
Some(resolved)
|
|
} else {
|
|
package_resolve(
|
|
specifier,
|
|
referrer,
|
|
NodeModuleKind::Esm,
|
|
conditions,
|
|
npm_resolver,
|
|
)?
|
|
.map(|p| ModuleSpecifier::from_file_path(p).unwrap())
|
|
};
|
|
Ok(match url {
|
|
Some(url) => Some(finalize_resolution(url, referrer)?),
|
|
None => None,
|
|
})
|
|
}
|
|
|
|
fn add_export(
|
|
source: &mut Vec<String>,
|
|
name: &str,
|
|
initializer: &str,
|
|
temp_var_count: &mut usize,
|
|
) {
|
|
fn is_valid_var_decl(name: &str) -> bool {
|
|
// it's ok to be super strict here
|
|
name
|
|
.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
|
|
}
|
|
|
|
// TODO(bartlomieju): Node actually checks if a given export exists in `exports` object,
|
|
// but it might not be necessary here since our analysis is more detailed?
|
|
if RESERVED_WORDS.contains(name) || !is_valid_var_decl(name) {
|
|
*temp_var_count += 1;
|
|
// we can't create an identifier with a reserved word or invalid identifier name,
|
|
// so assign it to a temporary variable that won't have a conflict, then re-export
|
|
// it as a string
|
|
source.push(format!(
|
|
"const __deno_export_{}__ = {};",
|
|
temp_var_count, initializer
|
|
));
|
|
source.push(format!(
|
|
"export {{ __deno_export_{}__ as \"{}\" }};",
|
|
temp_var_count, name
|
|
));
|
|
} else {
|
|
source.push(format!("export const {} = {};", name, initializer));
|
|
}
|
|
}
|
|
|
|
/// Translates given CJS module into ESM. This function will perform static
|
|
/// analysis on the file to find defined exports and reexports.
|
|
///
|
|
/// For all discovered reexports the analysis will be performed recursively.
|
|
///
|
|
/// If successful a source code for equivalent ES module is returned.
|
|
pub fn translate_cjs_to_esm(
|
|
file_fetcher: &FileFetcher,
|
|
specifier: &ModuleSpecifier,
|
|
code: String,
|
|
media_type: MediaType,
|
|
npm_resolver: &NpmPackageResolver,
|
|
node_analysis_cache: &NodeAnalysisCache,
|
|
) -> Result<String, AnyError> {
|
|
fn perform_cjs_analysis(
|
|
analysis_cache: &NodeAnalysisCache,
|
|
specifier: &str,
|
|
media_type: MediaType,
|
|
code: String,
|
|
) -> Result<CjsAnalysis, AnyError> {
|
|
let source_hash = NodeAnalysisCache::compute_source_hash(&code);
|
|
if let Some(analysis) =
|
|
analysis_cache.get_cjs_analysis(specifier, &source_hash)
|
|
{
|
|
return Ok(analysis);
|
|
}
|
|
|
|
if media_type == MediaType::Json {
|
|
return Ok(CjsAnalysis {
|
|
exports: vec![],
|
|
reexports: vec![],
|
|
});
|
|
}
|
|
|
|
let parsed_source = deno_ast::parse_script(deno_ast::ParseParams {
|
|
specifier: specifier.to_string(),
|
|
text_info: deno_ast::SourceTextInfo::new(code.into()),
|
|
media_type,
|
|
capture_tokens: true,
|
|
scope_analysis: false,
|
|
maybe_syntax: None,
|
|
})?;
|
|
let analysis = parsed_source.analyze_cjs();
|
|
analysis_cache.set_cjs_analysis(specifier, &source_hash, &analysis);
|
|
|
|
Ok(analysis)
|
|
}
|
|
|
|
let mut temp_var_count = 0;
|
|
let mut handled_reexports: HashSet<String> = HashSet::default();
|
|
|
|
let mut source = vec![
|
|
r#"const require = Deno[Deno.internal].require.Module.createRequire(import.meta.url);"#.to_string(),
|
|
];
|
|
|
|
let analysis = perform_cjs_analysis(
|
|
node_analysis_cache,
|
|
specifier.as_str(),
|
|
media_type,
|
|
code,
|
|
)?;
|
|
|
|
let mut all_exports = analysis
|
|
.exports
|
|
.iter()
|
|
.map(|s| s.to_string())
|
|
.collect::<HashSet<_>>();
|
|
|
|
// (request, referrer)
|
|
let mut reexports_to_handle = VecDeque::new();
|
|
for reexport in analysis.reexports {
|
|
reexports_to_handle.push_back((reexport, specifier.clone()));
|
|
}
|
|
|
|
while let Some((reexport, referrer)) = reexports_to_handle.pop_front() {
|
|
if handled_reexports.contains(&reexport) {
|
|
continue;
|
|
}
|
|
|
|
handled_reexports.insert(reexport.to_string());
|
|
|
|
// First, resolve relate reexport specifier
|
|
let resolved_reexport = resolve(
|
|
&reexport,
|
|
&referrer,
|
|
// FIXME(bartlomieju): check if these conditions are okay, probably
|
|
// should be `deno-require`, because `deno` is already used in `esm_resolver.rs`
|
|
&["deno", "require", "default"],
|
|
npm_resolver,
|
|
)?;
|
|
let reexport_specifier =
|
|
ModuleSpecifier::from_file_path(&resolved_reexport).unwrap();
|
|
// Second, read the source code from disk
|
|
let reexport_file = file_fetcher
|
|
.get_source(&reexport_specifier)
|
|
.ok_or_else(|| {
|
|
anyhow!(
|
|
"Could not find '{}' ({}) referenced from {}",
|
|
reexport,
|
|
reexport_specifier,
|
|
referrer
|
|
)
|
|
})?;
|
|
|
|
{
|
|
let analysis = perform_cjs_analysis(
|
|
node_analysis_cache,
|
|
reexport_specifier.as_str(),
|
|
reexport_file.media_type,
|
|
reexport_file.source.to_string(),
|
|
)?;
|
|
|
|
for reexport in analysis.reexports {
|
|
reexports_to_handle.push_back((reexport, reexport_specifier.clone()));
|
|
}
|
|
|
|
all_exports.extend(
|
|
analysis
|
|
.exports
|
|
.into_iter()
|
|
.filter(|e| e.as_str() != "default"),
|
|
);
|
|
}
|
|
}
|
|
|
|
source.push(format!(
|
|
"const mod = require(\"{}\");",
|
|
specifier
|
|
.to_file_path()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.replace('\\', "\\\\")
|
|
.replace('\'', "\\\'")
|
|
.replace('\"', "\\\"")
|
|
));
|
|
|
|
for export in &all_exports {
|
|
if export.as_str() != "default" {
|
|
add_export(
|
|
&mut source,
|
|
export,
|
|
&format!("mod[\"{}\"]", export),
|
|
&mut temp_var_count,
|
|
);
|
|
}
|
|
}
|
|
|
|
source.push("export default mod;".to_string());
|
|
|
|
let translated_source = source.join("\n");
|
|
Ok(translated_source)
|
|
}
|
|
|
|
fn resolve(
|
|
specifier: &str,
|
|
referrer: &ModuleSpecifier,
|
|
conditions: &[&str],
|
|
npm_resolver: &dyn RequireNpmResolver,
|
|
) -> Result<PathBuf, AnyError> {
|
|
if specifier.starts_with('/') {
|
|
todo!();
|
|
}
|
|
|
|
let referrer_path = referrer.to_file_path().unwrap();
|
|
if specifier.starts_with("./") || specifier.starts_with("../") {
|
|
if let Some(parent) = referrer_path.parent() {
|
|
return file_extension_probe(parent.join(specifier), &referrer_path);
|
|
} else {
|
|
todo!();
|
|
}
|
|
}
|
|
|
|
// We've got a bare specifier or maybe bare_specifier/blah.js"
|
|
|
|
let (package_specifier, package_subpath) =
|
|
parse_specifier(specifier).unwrap();
|
|
|
|
// todo(dsherret): use not_found error on not found here
|
|
let module_dir = npm_resolver.resolve_package_folder_from_package(
|
|
package_specifier.as_str(),
|
|
&referrer_path,
|
|
conditions,
|
|
)?;
|
|
|
|
let package_json_path = module_dir.join("package.json");
|
|
if package_json_path.exists() {
|
|
let package_json =
|
|
PackageJson::load(npm_resolver, package_json_path.clone())?;
|
|
|
|
if let Some(exports) = &package_json.exports {
|
|
return package_exports_resolve(
|
|
&package_json_path,
|
|
package_subpath,
|
|
exports,
|
|
referrer,
|
|
NodeModuleKind::Esm,
|
|
conditions,
|
|
npm_resolver,
|
|
);
|
|
}
|
|
|
|
// old school
|
|
if package_subpath != "." {
|
|
let d = module_dir.join(package_subpath);
|
|
if let Ok(m) = d.metadata() {
|
|
if m.is_dir() {
|
|
// subdir might have a package.json that specifies the entrypoint
|
|
let package_json_path = d.join("package.json");
|
|
if package_json_path.exists() {
|
|
let package_json =
|
|
PackageJson::load(npm_resolver, package_json_path)?;
|
|
if let Some(main) = package_json.main(NodeModuleKind::Cjs) {
|
|
return Ok(d.join(main).clean());
|
|
}
|
|
}
|
|
|
|
return Ok(d.join("index.js").clean());
|
|
}
|
|
}
|
|
return file_extension_probe(d, &referrer_path);
|
|
} else if let Some(main) = package_json.main(NodeModuleKind::Cjs) {
|
|
return Ok(module_dir.join(main).clean());
|
|
} else {
|
|
return Ok(module_dir.join("index.js").clean());
|
|
}
|
|
}
|
|
Err(not_found(specifier, &referrer_path))
|
|
}
|
|
|
|
fn parse_specifier(specifier: &str) -> Option<(String, String)> {
|
|
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('/').map(|i| i + index + 1);
|
|
} 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 None;
|
|
}
|
|
|
|
let package_subpath = if let Some(index) = separator_index {
|
|
format!(".{}", specifier.chars().skip(index).collect::<String>())
|
|
} else {
|
|
".".to_string()
|
|
};
|
|
|
|
Some((package_name, package_subpath))
|
|
}
|
|
|
|
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 should_be_treated_as_relative_or_absolute_path(specifier: &str) -> bool {
|
|
if specifier.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
if specifier.starts_with('/') {
|
|
return true;
|
|
}
|
|
|
|
is_relative_specifier(specifier)
|
|
}
|
|
|
|
// TODO(ry) We very likely have this utility function elsewhere in Deno.
|
|
fn is_relative_specifier(specifier: &str) -> bool {
|
|
let specifier_len = specifier.len();
|
|
let specifier_chars: Vec<_> = specifier.chars().collect();
|
|
|
|
if !specifier_chars.is_empty() && specifier_chars[0] == '.' {
|
|
if specifier_len == 1 || specifier_chars[1] == '/' {
|
|
return true;
|
|
}
|
|
if specifier_chars[1] == '.'
|
|
&& (specifier_len == 2 || specifier_chars[2] == '/')
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn file_extension_probe(
|
|
p: PathBuf,
|
|
referrer: &Path,
|
|
) -> Result<PathBuf, AnyError> {
|
|
let p = p.clean();
|
|
if p.exists() {
|
|
let file_name = p.file_name().unwrap();
|
|
let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
|
|
if p_js.exists() && p_js.is_file() {
|
|
return Ok(p_js);
|
|
} else if p.is_dir() {
|
|
return Ok(p.join("index.js"));
|
|
} else {
|
|
return Ok(p);
|
|
}
|
|
} else if let Some(file_name) = p.file_name() {
|
|
let p_js = p.with_file_name(format!("{}.js", file_name.to_str().unwrap()));
|
|
if p_js.exists() && p_js.is_file() {
|
|
return Ok(p_js);
|
|
}
|
|
}
|
|
Err(not_found(&p.to_string_lossy(), referrer))
|
|
}
|
|
|
|
fn not_found(path: &str, referrer: &Path) -> AnyError {
|
|
let msg = format!(
|
|
"[ERR_MODULE_NOT_FOUND] Cannot find module \"{}\" imported from \"{}\"",
|
|
path,
|
|
referrer.to_string_lossy()
|
|
);
|
|
std::io::Error::new(std::io::ErrorKind::NotFound, msg).into()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use deno_core::serde_json::json;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_add_export() {
|
|
let mut temp_var_count = 0;
|
|
let mut source = vec![];
|
|
|
|
let exports = vec!["static", "server", "app", "dashed-export"];
|
|
for export in exports {
|
|
add_export(&mut source, export, "init", &mut temp_var_count);
|
|
}
|
|
assert_eq!(
|
|
source,
|
|
vec![
|
|
"const __deno_export_1__ = init;".to_string(),
|
|
"export { __deno_export_1__ as \"static\" };".to_string(),
|
|
"export const server = init;".to_string(),
|
|
"export const app = init;".to_string(),
|
|
"const __deno_export_2__ = init;".to_string(),
|
|
"export { __deno_export_2__ as \"dashed-export\" };".to_string(),
|
|
]
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_specifier() {
|
|
assert_eq!(
|
|
parse_specifier("@some-package/core/actions"),
|
|
Some(("@some-package/core".to_string(), "./actions".to_string()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_bin_entry_value() {
|
|
// should resolve the specified value
|
|
let value = json!({
|
|
"bin1": "./value1",
|
|
"bin2": "./value2",
|
|
"test": "./value3",
|
|
});
|
|
assert_eq!(
|
|
resolve_bin_entry_value(
|
|
&NpmPackageReq::from_str("test").unwrap(),
|
|
Some("bin1"),
|
|
&value
|
|
)
|
|
.unwrap(),
|
|
"./value1"
|
|
);
|
|
|
|
// should resolve the value with the same name when not specified
|
|
assert_eq!(
|
|
resolve_bin_entry_value(
|
|
&NpmPackageReq::from_str("test").unwrap(),
|
|
None,
|
|
&value
|
|
)
|
|
.unwrap(),
|
|
"./value3"
|
|
);
|
|
|
|
// should not resolve when specified value does not exist
|
|
assert_eq!(
|
|
resolve_bin_entry_value(
|
|
&NpmPackageReq::from_str("test").unwrap(),
|
|
Some("other"),
|
|
&value
|
|
)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
concat!(
|
|
"package 'test' did not have a bin entry for 'other' in its package.json\n",
|
|
"\n",
|
|
"Possibilities:\n",
|
|
" * npm:test/bin1\n",
|
|
" * npm:test/bin2\n",
|
|
" * npm:test/test"
|
|
)
|
|
);
|
|
|
|
// should not resolve when default value can't be determined
|
|
assert_eq!(
|
|
resolve_bin_entry_value(
|
|
&NpmPackageReq::from_str("asdf@1.2").unwrap(),
|
|
None,
|
|
&value
|
|
)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
concat!(
|
|
"package 'asdf' did not have a bin entry for 'asdf' in its package.json\n",
|
|
"\n",
|
|
"Possibilities:\n",
|
|
" * npm:asdf@1.2/bin1\n",
|
|
" * npm:asdf@1.2/bin2\n",
|
|
" * npm:asdf@1.2/test"
|
|
)
|
|
);
|
|
|
|
// should resolve since all the values are the same
|
|
let value = json!({
|
|
"bin1": "./value",
|
|
"bin2": "./value",
|
|
});
|
|
assert_eq!(
|
|
resolve_bin_entry_value(
|
|
&NpmPackageReq::from_str("test").unwrap(),
|
|
None,
|
|
&value
|
|
)
|
|
.unwrap(),
|
|
"./value"
|
|
);
|
|
|
|
// should not resolve when specified and is a string
|
|
let value = json!("./value");
|
|
assert_eq!(
|
|
resolve_bin_entry_value(
|
|
&NpmPackageReq::from_str("test").unwrap(),
|
|
Some("path"),
|
|
&value
|
|
)
|
|
.err()
|
|
.unwrap()
|
|
.to_string(),
|
|
"package 'test' did not have a bin entry for 'path' in its package.json"
|
|
);
|
|
}
|
|
}
|