summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/compat.rs88
-rw-r--r--cli/compat/errors.rs145
-rw-r--r--cli/compat/esm_resolver.rs1182
-rw-r--r--cli/compat/mod.rs132
-rw-r--r--cli/compat/testdata/basic/main.js1
-rw-r--r--cli/compat/testdata/basic/node_modules/foo/index.js0
-rw-r--r--cli/compat/testdata/basic/node_modules/foo/package.json5
-rw-r--r--cli/compat/testdata/basic/package.json7
-rw-r--r--cli/compat/testdata/basic_deps/main.js1
-rw-r--r--cli/compat/testdata/basic_deps/node_modules/bar/bar.js1
-rw-r--r--cli/compat/testdata/basic_deps/node_modules/bar/package.json6
-rw-r--r--cli/compat/testdata/basic_deps/node_modules/foo/foo.js1
-rw-r--r--cli/compat/testdata/basic_deps/node_modules/foo/package.json8
-rw-r--r--cli/compat/testdata/basic_deps/package.json7
-rw-r--r--cli/compat/testdata/conditions/main.js1
-rw-r--r--cli/compat/testdata/conditions/node_modules/imports_exports/import_export.js6
-rw-r--r--cli/compat/testdata/conditions/node_modules/imports_exports/import_polyfill.js3
-rw-r--r--cli/compat/testdata/conditions/node_modules/imports_exports/package.json17
-rw-r--r--cli/compat/testdata/conditions/node_modules/imports_exports/require_export.cjs6
-rw-r--r--cli/compat/testdata/conditions/node_modules/imports_exports/require_polyfill.js3
-rw-r--r--cli/compat/testdata/conditions/package.json7
-rw-r--r--cli/compat/testdata/deep/a/b/c/d/main.js1
-rw-r--r--cli/compat/testdata/deep/node_modules/foo/index.js0
-rw-r--r--cli/compat/testdata/deep/node_modules/foo/package.json5
-rw-r--r--cli/main.rs38
-rw-r--r--cli/proc_state.rs48
-rw-r--r--cli/tests/integration/compat_tests.rs10
-rw-r--r--cli/tests/testdata/compat/existing_import_map.json5
-rw-r--r--cli/tests/testdata/compat/existing_import_map.out7
-rw-r--r--cli/tests/testdata/compat/fs_promises.mjs (renamed from cli/tests/testdata/compat/fs_promises.js)0
-rw-r--r--cli/tests/testdata/compat/globals.out4
-rw-r--r--cli/tests/testdata/compat/node_fs_promises.mjs (renamed from cli/tests/testdata/compat/node_fs_promises.js)0
32 files changed, 1606 insertions, 139 deletions
diff --git a/cli/compat.rs b/cli/compat.rs
deleted file mode 100644
index f4cc2a08d..000000000
--- a/cli/compat.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
-
-use deno_core::url::Url;
-use std::collections::HashMap;
-
-// TODO(bartlomieju): this needs to be bumped manually for
-// each release, a better mechanism is preferable, but it's a quick and dirty
-// solution to avoid printing `X-Deno-Warning` headers when the compat layer is
-// downloaded
-static STD_URL: &str = "https://deno.land/std@0.111.0/";
-static GLOBAL_MODULE: &str = "global.ts";
-
-static SUPPORTED_MODULES: &[&str] = &[
- "assert",
- "assert/strict",
- "async_hooks",
- "buffer",
- "child_process",
- "cluster",
- "console",
- "constants",
- "crypto",
- "dgram",
- "dns",
- "domain",
- "events",
- "fs",
- "fs/promises",
- "http",
- "https",
- "module",
- "net",
- "os",
- "path",
- "path/posix",
- "path/win32",
- "perf_hooks",
- "process",
- "querystring",
- "readline",
- "stream",
- "stream/promises",
- "stream/web",
- "string_decoder",
- "sys",
- "timers",
- "timers/promises",
- "tls",
- "tty",
- "url",
- "util",
- "util/types",
- "v8",
- "vm",
- "zlib",
-];
-
-lazy_static::lazy_static! {
- static ref GLOBAL_URL_STR: String = format!("{}node/{}", STD_URL, GLOBAL_MODULE);
- pub(crate) static ref GLOBAL_URL: Url = Url::parse(&GLOBAL_URL_STR).unwrap();
- static ref COMPAT_IMPORT_URL: Url = Url::parse("flags:compat").unwrap();
-}
-
-/// Provide imports into a module graph when the compat flag is true.
-pub(crate) fn get_node_imports() -> Vec<(Url, Vec<String>)> {
- vec![(COMPAT_IMPORT_URL.clone(), vec![GLOBAL_URL_STR.clone()])]
-}
-
-/// Create a map that can be used to update import map.
-///
-/// Keys are built-in Node modules (and built-ins prefixed with "node:"), while
-/// values are URLs pointing to relevant files in deno.land/std/node/ directory.
-pub fn get_mapped_node_builtins() -> HashMap<String, String> {
- let mut mappings = HashMap::new();
-
- for module in SUPPORTED_MODULES {
- // TODO(bartlomieju): this is unversioned, and should be fixed to use latest stable?
- let module_url = format!("{}node/{}.ts", STD_URL, module);
- mappings.insert(module.to_string(), module_url.clone());
-
- // Support for `node:<module_name>`
- // https://nodejs.org/api/esm.html#esm_node_imports
- let node_prefixed = format!("node:{}", module);
- mappings.insert(node_prefixed, module_url);
- }
-
- mappings
-}
diff --git a/cli/compat/errors.rs b/cli/compat/errors.rs
new file mode 100644
index 000000000..3a44b2c88
--- /dev/null
+++ b/cli/compat/errors.rs
@@ -0,0 +1,145 @@
+// Copyright 2018-2021 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;
+use deno_core::url::Url;
+
+pub(crate) 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)
+}
+
+pub(crate) 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)
+}
+
+pub(crate) 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(crate) fn err_unsupported_dir_import(path: &str, base: &str) -> AnyError {
+ generic_error(format!("[ERR_UNSUPPORTED_DIR_IMPORT] Directory import '{}' is not supported resolving ES modules imported from {}", path, base))
+}
+
+pub(crate) fn err_unsupported_esm_url_scheme(url: &Url) -> AnyError {
+ let mut msg =
+ "[ERR_UNSUPPORTED_ESM_URL_SCHEME] Only file and data URLS are supported by the default ESM loader"
+ .to_string();
+
+ if cfg!(window) && url.scheme().len() == 2 {
+ msg = format!(
+ "{}. On Windows, absolute path must be valid file:// URLs",
+ msg
+ );
+ }
+
+ msg = format!("{}. Received protocol '{}'", msg, url.scheme());
+ generic_error(msg)
+}
+
+pub(crate) 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(crate) 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(crate) 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/cli/compat/esm_resolver.rs b/cli/compat/esm_resolver.rs
new file mode 100644
index 000000000..c0366c27b
--- /dev/null
+++ b/cli/compat/esm_resolver.rs
@@ -0,0 +1,1182 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+use super::errors;
+use deno_core::error::generic_error;
+use deno_core::error::AnyError;
+use deno_core::serde_json;
+use deno_core::serde_json::Map;
+use deno_core::serde_json::Value;
+use deno_core::url::Url;
+use deno_core::ModuleSpecifier;
+use deno_graph::source::Resolver;
+use regex::Regex;
+use std::path::PathBuf;
+
+#[derive(Debug, Default)]
+pub struct NodeEsmResolver;
+
+impl NodeEsmResolver {
+ pub fn as_resolver(&self) -> &dyn Resolver {
+ self
+ }
+}
+
+impl Resolver for NodeEsmResolver {
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ ) -> Result<ModuleSpecifier, AnyError> {
+ node_resolve(specifier, referrer.as_str(), &std::env::current_dir()?)
+ }
+}
+
+static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"];
+
+/// This function is an implementation of `defaultResolve` in
+/// `lib/internal/modules/esm/resolve.js` from Node.
+fn node_resolve(
+ specifier: &str,
+ referrer: &str,
+ cwd: &std::path::Path,
+) -> Result<ModuleSpecifier, AnyError> {
+ // TODO(bartlomieju): skipped "policy" part as we don't plan to support it
+
+ if let Some(resolved) = crate::compat::try_resolve_builtin_module(specifier) {
+ return Ok(resolved);
+ }
+
+ if let Ok(url) = Url::parse(specifier) {
+ if url.scheme() == "data" {
+ return Ok(url);
+ }
+
+ let protocol = url.scheme();
+
+ if protocol == "node" {
+ let split_specifier = url.as_str().split(':');
+ let specifier = split_specifier.skip(1).collect::<Vec<_>>().join("");
+ if let Some(resolved) =
+ crate::compat::try_resolve_builtin_module(&specifier)
+ {
+ return Ok(resolved);
+ } else {
+ return Err(generic_error(format!("Unknown module {}", specifier)));
+ }
+ }
+
+ if protocol != "file" && protocol != "data" {
+ return Err(errors::err_unsupported_esm_url_scheme(&url));
+ }
+
+ if referrer.starts_with("data:") {
+ let referrer_url = Url::parse(referrer)?;
+ return referrer_url.join(specifier).map_err(AnyError::from);
+ }
+ }
+
+ let is_main = referrer.is_empty();
+ let parent_url = if is_main {
+ Url::from_directory_path(cwd).unwrap()
+ } else {
+ Url::parse(referrer).expect("referrer was not proper url")
+ };
+
+ let conditions = DEFAULT_CONDITIONS;
+ let url = module_resolve(specifier, &parent_url, conditions)?;
+
+ // TODO(bartlomieju): skipped checking errors for commonJS resolution and
+ // "preserveSymlinksMain"/"preserveSymlinks" options.
+ Ok(url)
+}
+
+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 module_resolve(
+ specifier: &str,
+ base: &ModuleSpecifier,
+ conditions: &[&str],
+) -> Result<ModuleSpecifier, AnyError> {
+ let resolved = if should_be_treated_as_relative_or_absolute_path(specifier) {
+ base.join(specifier)?
+ } else if specifier.starts_with('#') {
+ package_imports_resolve(specifier, base, conditions)?
+ } else if let Ok(resolved) = Url::parse(specifier) {
+ resolved
+ } else {
+ package_resolve(specifier, base, conditions)?
+ };
+ finalize_resolution(resolved, base)
+}
+
+fn finalize_resolution(
+ resolved: ModuleSpecifier,
+ base: &ModuleSpecifier,
+) -> Result<ModuleSpecifier, AnyError> {
+ // TODO(bartlomieju): this is not part of Node resolution algorithm
+ // (as it doesn't support http/https); but I had to short circuit here
+ // for remote modules because they are mainly used to polyfill `node` built
+ // in modules. Another option would be to leave the resolved URLs
+ // as `node:<module_name>` and do the actual remapping to std's polyfill
+ // in module loader. I'm not sure which approach is better.
+ if resolved.scheme().starts_with("http") {
+ return Ok(resolved);
+ }
+
+ let encoded_sep_re = Regex::new(r"%2F|%2C").expect("bad regex");
+
+ 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(
+ &path.display().to_string(),
+ &to_file_path_string(base),
+ ));
+ } else if !is_file {
+ return Err(errors::err_module_not_found(
+ &path.display().to_string(),
+ &to_file_path_string(base),
+ "module",
+ ));
+ }
+
+ Ok(resolved)
+}
+
+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
+}
+
+fn package_imports_resolve(
+ name: &str,
+ base: &ModuleSpecifier,
+ conditions: &[&str],
+) -> 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(base)),
+ ));
+ }
+
+ let mut package_json_url = None;
+
+ let package_config = get_package_scope_config(base)?;
+ if package_config.exists {
+ package_json_url =
+ Some(Url::from_file_path(package_config.pjsonpath).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(),
+ base,
+ false,
+ true,
+ conditions,
+ )?;
+ 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(),
+ base,
+ true,
+ true,
+ conditions,
+ )?;
+ if let Some(resolved) = maybe_resolved {
+ return Ok(resolved);
+ }
+ }
+ }
+ }
+ }
+
+ Err(throw_import_not_defined(name, package_json_url, base))
+}
+
+fn is_conditional_exports_main_sugar(
+ exports: &Value,
+ package_json_url: &ModuleSpecifier,
+ base: &ModuleSpecifier,
+) -> Result<bool, AnyError> {
+ if exports.is_string() || exports.is_array() {
+ return Ok(true);
+ }
+
+ if exports.is_null() || !exports.is_object() {
+ return Ok(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 {
+ return Err(errors::err_invalid_package_config(
+ &to_file_path_string(package_json_url),
+ Some(base.as_str().to_string()),
+ Some("\"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.".to_string())
+ ));
+ }
+ }
+
+ Ok(is_conditional_sugar)
+}
+
+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],
+) -> 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);
+ }
+ }
+ 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],
+) -> 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,
+ )?));
+ } 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,
+ );
+
+ 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,
+ )?;
+ 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)),
+ )
+}
+
+fn package_exports_resolve(
+ package_json_url: ModuleSpecifier,
+ package_subpath: String,
+ package_config: PackageConfig,
+ base: &ModuleSpecifier,
+ conditions: &[&str],
+) -> Result<ModuleSpecifier, AnyError> {
+ let exports = &package_config.exports.unwrap();
+
+ let exports_map =
+ if is_conditional_exports_main_sugar(exports, &package_json_url, base)? {
+ let mut map = Map::new();
+ map.insert(".".to_string(), exports.to_owned());
+ map
+ } else {
+ exports.as_object().unwrap().to_owned()
+ };
+
+ if exports_map.contains_key(&package_subpath)
+ && package_subpath.find('*').is_none()
+ && !package_subpath.ends_with('/')
+ {
+ let target = exports_map.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,
+ )?;
+ 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 exports_map.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 = 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,
+ )?;
+ 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 package_resolve(
+ specifier: &str,
+ base: &ModuleSpecifier,
+ conditions: &[&str],
+) -> Result<ModuleSpecifier, AnyError> {
+ let (package_name, package_subpath, is_scoped) =
+ parse_package_name(specifier, base)?;
+
+ // ResolveSelf
+ let package_config = get_package_scope_config(base)?;
+ if package_config.exists {
+ let package_json_url =
+ Url::from_file_path(&package_config.pjsonpath).unwrap();
+ if package_config.name.as_ref() == Some(&package_name) {
+ if let Some(exports) = &package_config.exports {
+ if !exports.is_null() {
+ return package_exports_resolve(
+ package_json_url,
+ package_subpath,
+ package_config,
+ base,
+ conditions,
+ );
+ }
+ }
+ }
+ }
+
+ let mut package_json_url =
+ base.join(&format!("./node_modules/{}/package.json", package_name))?;
+ let mut package_json_path = to_file_path(&package_json_url);
+ let mut last_path;
+ loop {
+ let p_str = package_json_path.to_str().unwrap();
+ let package_str_len = "/package.json".len();
+ let p = p_str[0..=p_str.len() - package_str_len].to_string();
+ let is_dir = if let Ok(stats) = std::fs::metadata(&p) {
+ stats.is_dir()
+ } else {
+ false
+ };
+ if !is_dir {
+ last_path = package_json_path;
+
+ let prefix = if is_scoped {
+ "../../../../node_modules/"
+ } else {
+ "../../../node_modules/"
+ };
+ package_json_url = package_json_url
+ .join(&format!("{}{}/package.json", prefix, package_name))?;
+ package_json_path = to_file_path(&package_json_url);
+ if package_json_path.to_str().unwrap().len()
+ == last_path.to_str().unwrap().len()
+ {
+ break;
+ } else {
+ continue;
+ }
+ }
+
+ // Package match.
+ let package_config =
+ get_package_config(package_json_path.clone(), specifier, Some(base))?;
+ if package_config.exports.is_some() {
+ return package_exports_resolve(
+ package_json_url,
+ package_subpath,
+ package_config,
+ base,
+ conditions,
+ );
+ }
+ if package_subpath == "." {
+ return legacy_main_resolve(&package_json_url, &package_config, base);
+ }
+
+ return package_json_url
+ .join(&package_subpath)
+ .map_err(AnyError::from);
+ }
+
+ Err(errors::err_module_not_found(
+ &package_json_url
+ .join(".")
+ .unwrap()
+ .to_file_path()
+ .unwrap()
+ .display()
+ .to_string(),
+ &to_file_path_string(base),
+ "package",
+ ))
+}
+
+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))
+}
+
+#[derive(Clone, Debug)]
+struct PackageConfig {
+ exists: bool,
+ exports: Option<Value>,
+ imports: Option<Map<String, Value>>,
+ main: Option<String>,
+ name: Option<String>,
+ pjsonpath: PathBuf,
+ typ: String,
+}
+
+fn get_package_config(
+ path: PathBuf,
+ specifier: &str,
+ maybe_base: Option<&ModuleSpecifier>,
+) -> Result<PackageConfig, AnyError> {
+ // TODO(bartlomieju):
+ // if let Some(existing) = package_json_cache.get(path) {
+ // return Ok(existing.clone());
+ // }
+
+ let result = std::fs::read_to_string(&path);
+
+ let source = result.unwrap_or_else(|_| "".to_string());
+ if source.is_empty() {
+ let package_config = PackageConfig {
+ pjsonpath: path,
+ exists: false,
+ main: None,
+ name: None,
+ typ: "none".to_string(),
+ exports: None,
+ imports: None,
+ };
+ // TODO(bartlomieju):
+ // package_json_cache.set(package_json_path, package_config.clone());
+ return Ok(package_config);
+ }
+
+ let package_json: Value = serde_json::from_str(&source).map_err(|err| {
+ let base_msg = maybe_base.map(|base| {
+ format!("\"{}\" from {}", specifier, to_file_path(base).display())
+ });
+ errors::err_invalid_package_config(
+ &path.display().to_string(),
+ base_msg,
+ Some(err.to_string()),
+ )
+ })?;
+
+ let imports_val = package_json.get("imports");
+ let main_val = package_json.get("main");
+ let name_val = package_json.get("name");
+ let typ_val = package_json.get("type");
+ let exports = package_json.get("exports").map(|e| e.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) = typ_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()
+ };
+
+ let package_config = PackageConfig {
+ pjsonpath: path,
+ exists: true,
+ main,
+ name,
+ typ,
+ exports,
+ imports,
+ };
+ // TODO(bartlomieju):
+ // package_json_cache.set(package_json_path, package_config.clone());
+ Ok(package_config)
+}
+
+fn get_package_scope_config(
+ resolved: &ModuleSpecifier,
+) -> Result<PackageConfig, AnyError> {
+ let mut package_json_url = resolved.join("./package.json")?;
+
+ loop {
+ let package_json_path = package_json_url.path();
+
+ if package_json_path.ends_with("node_modules/package.json") {
+ break;
+ }
+
+ let package_config = get_package_config(
+ to_file_path(&package_json_url),
+ resolved.as_str(),
+ None,
+ )?;
+
+ if package_config.exists {
+ return Ok(package_config);
+ }
+
+ let last_package_json_url = package_json_url.clone();
+ package_json_url = package_json_url.join("../package.json")?;
+
+ // TODO(bartlomieju): I'm not sure this will work properly
+ // Terminates at root where ../package.json equals ../../package.json
+ // (can't just check "/package.json" for Windows support)
+ if package_json_url.path() == last_package_json_url.path() {
+ break;
+ }
+ }
+
+ let package_json_path = to_file_path(&package_json_url);
+ let package_config = PackageConfig {
+ pjsonpath: package_json_path,
+ exists: false,
+ main: None,
+ name: None,
+ typ: "none".to_string(),
+ exports: None,
+ imports: None,
+ };
+
+ // TODO(bartlomieju):
+ // package_json_cache.set(package_json_path, package_config.clone());
+
+ Ok(package_config)
+}
+
+fn file_exists(path_url: &ModuleSpecifier) -> bool {
+ if let Ok(stats) = std::fs::metadata(to_file_path(path_url)) {
+ stats.is_file()
+ } else {
+ false
+ }
+}
+
+fn legacy_main_resolve(
+ package_json_url: &ModuleSpecifier,
+ package_config: &PackageConfig,
+ _base: &ModuleSpecifier,
+) -> Result<ModuleSpecifier, AnyError> {
+ let mut guess;
+
+ if let Some(main) = &package_config.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"))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn testdir(name: &str) -> PathBuf {
+ let c = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ c.join("compat/testdata/").join(name)
+ }
+
+ #[test]
+ fn basic() {
+ let cwd = testdir("basic");
+ let main = Url::from_file_path(cwd.join("main.js")).unwrap();
+ let actual = node_resolve("foo", main.as_str(), &cwd).unwrap();
+ let expected =
+ Url::from_file_path(cwd.join("node_modules/foo/index.js")).unwrap();
+ assert_eq!(actual, expected);
+
+ let actual = node_resolve(
+ "data:application/javascript,console.log(\"Hello%20Deno\");",
+ main.as_str(),
+ &cwd,
+ )
+ .unwrap();
+ eprintln!("actual {}", actual);
+ assert_eq!(
+ actual,
+ Url::parse("data:application/javascript,console.log(\"Hello%20Deno\");")
+ .unwrap()
+ );
+ }
+
+ #[test]
+ fn deep() {
+ let cwd = testdir("deep");
+ let main = Url::from_file_path(cwd.join("a/b/c/d/main.js")).unwrap();
+ let actual = node_resolve("foo", main.as_str(), &cwd).unwrap();
+ let expected =
+ Url::from_file_path(cwd.join("node_modules/foo/index.js")).unwrap();
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn basic_deps() {
+ let cwd = testdir("basic_deps");
+ let main = Url::from_file_path(cwd.join("main.js")).unwrap();
+ let actual = node_resolve("foo", main.as_str(), &cwd).unwrap();
+ let foo_js =
+ Url::from_file_path(cwd.join("node_modules/foo/foo.js")).unwrap();
+ assert_eq!(actual, foo_js);
+
+ let actual = node_resolve("bar", foo_js.as_str(), &cwd).unwrap();
+
+ let bar_js =
+ Url::from_file_path(cwd.join("node_modules/bar/bar.js")).unwrap();
+ assert_eq!(actual, bar_js);
+ }
+
+ #[test]
+ fn builtin_http() {
+ let cwd = testdir("basic");
+ let main = Url::from_file_path(cwd.join("main.js")).unwrap();
+ let expected =
+ Url::parse("https://deno.land/std@0.112.0/node/http.ts").unwrap();
+
+ let actual = node_resolve("http", main.as_str(), &cwd).unwrap();
+ println!("actual {}", actual);
+ assert_eq!(actual, expected);
+
+ let actual = node_resolve("node:http", main.as_str(), &cwd).unwrap();
+ println!("actual {}", actual);
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn conditional_exports() {
+ // check that `exports` mapping works correctly
+ let cwd = testdir("conditions");
+ let main = Url::from_file_path(cwd.join("main.js")).unwrap();
+ let actual = node_resolve("imports_exports", main.as_str(), &cwd).unwrap();
+ let expected = Url::from_file_path(
+ cwd.join("node_modules/imports_exports/import_export.js"),
+ )
+ .unwrap();
+ assert_eq!(actual, expected);
+
+ // check that `imports` mapping works correctly
+ let cwd = testdir("conditions/node_modules/imports_exports");
+ let main = Url::from_file_path(cwd.join("import_export.js")).unwrap();
+ let actual = node_resolve("#dep", main.as_str(), &cwd).unwrap();
+ let expected = Url::from_file_path(cwd.join("import_polyfill.js")).unwrap();
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn test_is_relative_specifier() {
+ assert!(is_relative_specifier("./foo.js"));
+ assert!(!is_relative_specifier("https://deno.land/std/node/http.ts"));
+ }
+}
diff --git a/cli/compat/mod.rs b/cli/compat/mod.rs
new file mode 100644
index 000000000..b95b65ddb
--- /dev/null
+++ b/cli/compat/mod.rs
@@ -0,0 +1,132 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+mod errors;
+mod esm_resolver;
+
+use deno_core::error::AnyError;
+use deno_core::located_script_name;
+use deno_core::url::Url;
+use deno_core::JsRuntime;
+
+pub use esm_resolver::NodeEsmResolver;
+
+// TODO(bartlomieju): this needs to be bumped manually for
+// each release, a better mechanism is preferable, but it's a quick and dirty
+// solution to avoid printing `X-Deno-Warning` headers when the compat layer is
+// downloaded
+static STD_URL_STR: &str = "https://deno.land/std@0.112.0/";
+
+static SUPPORTED_MODULES: &[&str] = &[
+ "assert",
+ "assert/strict",
+ "async_hooks",
+ "buffer",
+ "child_process",
+ "cluster",
+ "console",
+ "constants",
+ "crypto",
+ "dgram",
+ "dns",
+ "domain",
+ "events",
+ "fs",
+ "fs/promises",
+ "http",
+ "https",
+ "module",
+ "net",
+ "os",
+ "path",
+ "path/posix",
+ "path/win32",
+ "perf_hooks",
+ "process",
+ "querystring",
+ "readline",
+ "stream",
+ "stream/promises",
+ "stream/web",
+ "string_decoder",
+ "sys",
+ "timers",
+ "timers/promises",
+ "tls",
+ "tty",
+ "url",
+ "util",
+ "util/types",
+ "v8",
+ "vm",
+ "zlib",
+];
+
+lazy_static::lazy_static! {
+ static ref GLOBAL_URL_STR: String = format!("{}node/global.ts", STD_URL_STR);
+ pub(crate) static ref GLOBAL_URL: Url = Url::parse(&GLOBAL_URL_STR).unwrap();
+ static ref MODULE_URL_STR: String = format!("{}node/module.ts", STD_URL_STR);
+ pub(crate) static ref MODULE_URL: Url = Url::parse(&MODULE_URL_STR).unwrap();
+ static ref COMPAT_IMPORT_URL: Url = Url::parse("flags:compat").unwrap();
+}
+
+/// Provide imports into a module graph when the compat flag is true.
+pub(crate) fn get_node_imports() -> Vec<(Url, Vec<String>)> {
+ vec![(COMPAT_IMPORT_URL.clone(), vec![GLOBAL_URL_STR.clone()])]
+}
+
+fn try_resolve_builtin_module(specifier: &str) -> Option<Url> {
+ if SUPPORTED_MODULES.contains(&specifier) {
+ let module_url = format!("{}node/{}.ts", STD_URL_STR, specifier);
+ Some(Url::parse(&module_url).unwrap())
+ } else {
+ None
+ }
+}
+
+pub async fn check_if_should_use_esm_loader(
+ js_runtime: &mut JsRuntime,
+ main_module: &str,
+) -> Result<bool, AnyError> {
+ // Decide if we're running with Node ESM loader or CJS loader.
+ let source_code = &format!(
+ r#"(async function checkIfEsm(main) {{
+ const {{ resolveMainPath, shouldUseESMLoader }} = await import("{}");
+ const resolvedMain = resolveMainPath(main);
+ const useESMLoader = shouldUseESMLoader(resolvedMain);
+ return useESMLoader;
+ }})('{}');"#,
+ MODULE_URL_STR.as_str(),
+ escape_for_single_quote_string(main_module),
+ );
+ let result =
+ js_runtime.execute_script(&located_script_name!(), source_code)?;
+ let use_esm_loader_global = js_runtime.resolve_value(result).await?;
+ let use_esm_loader = {
+ let scope = &mut js_runtime.handle_scope();
+ let use_esm_loader_local = use_esm_loader_global.get(scope);
+ use_esm_loader_local.boolean_value(scope)
+ };
+
+ Ok(use_esm_loader)
+}
+
+pub fn load_cjs_module(
+ js_runtime: &mut JsRuntime,
+ main_module: &str,
+) -> Result<(), AnyError> {
+ let source_code = &format!(
+ r#"(async function loadCjsModule(main) {{
+ const Module = await import("{}");
+ Module.default._load(main, null, true);
+ }})('{}');"#,
+ MODULE_URL_STR.as_str(),
+ escape_for_single_quote_string(main_module),
+ );
+
+ js_runtime.execute_script(&located_script_name!(), source_code)?;
+ Ok(())
+}
+
+fn escape_for_single_quote_string(text: &str) -> String {
+ text.replace(r"\", r"\\").replace("'", r"\'")
+}
diff --git a/cli/compat/testdata/basic/main.js b/cli/compat/testdata/basic/main.js
new file mode 100644
index 000000000..c0748305d
--- /dev/null
+++ b/cli/compat/testdata/basic/main.js
@@ -0,0 +1 @@
+import "foo";
diff --git a/cli/compat/testdata/basic/node_modules/foo/index.js b/cli/compat/testdata/basic/node_modules/foo/index.js
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/cli/compat/testdata/basic/node_modules/foo/index.js
diff --git a/cli/compat/testdata/basic/node_modules/foo/package.json b/cli/compat/testdata/basic/node_modules/foo/package.json
new file mode 100644
index 000000000..a74d52fd3
--- /dev/null
+++ b/cli/compat/testdata/basic/node_modules/foo/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "foo",
+ "type": "module",
+ "exports": "./index.js"
+}
diff --git a/cli/compat/testdata/basic/package.json b/cli/compat/testdata/basic/package.json
new file mode 100644
index 000000000..cc4ac5493
--- /dev/null
+++ b/cli/compat/testdata/basic/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "bar",
+ "type": "module",
+ "dependencies": {
+ "foo": "1.0.0"
+ }
+}
diff --git a/cli/compat/testdata/basic_deps/main.js b/cli/compat/testdata/basic_deps/main.js
new file mode 100644
index 000000000..c0748305d
--- /dev/null
+++ b/cli/compat/testdata/basic_deps/main.js
@@ -0,0 +1 @@
+import "foo";
diff --git a/cli/compat/testdata/basic_deps/node_modules/bar/bar.js b/cli/compat/testdata/basic_deps/node_modules/bar/bar.js
new file mode 100644
index 000000000..98e51675e
--- /dev/null
+++ b/cli/compat/testdata/basic_deps/node_modules/bar/bar.js
@@ -0,0 +1 @@
+export const BAR = 123;
diff --git a/cli/compat/testdata/basic_deps/node_modules/bar/package.json b/cli/compat/testdata/basic_deps/node_modules/bar/package.json
new file mode 100644
index 000000000..c2043f610
--- /dev/null
+++ b/cli/compat/testdata/basic_deps/node_modules/bar/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "bar",
+ "version": "0.1.2",
+ "type": "module",
+ "exports": "./bar.js"
+}
diff --git a/cli/compat/testdata/basic_deps/node_modules/foo/foo.js b/cli/compat/testdata/basic_deps/node_modules/foo/foo.js
new file mode 100644
index 000000000..0026acc8e
--- /dev/null
+++ b/cli/compat/testdata/basic_deps/node_modules/foo/foo.js
@@ -0,0 +1 @@
+import "bar";
diff --git a/cli/compat/testdata/basic_deps/node_modules/foo/package.json b/cli/compat/testdata/basic_deps/node_modules/foo/package.json
new file mode 100644
index 000000000..376dae81e
--- /dev/null
+++ b/cli/compat/testdata/basic_deps/node_modules/foo/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "foo",
+ "type": "module",
+ "exports": "./foo.js",
+ "dependencies": {
+ "bar": "0.1.2"
+ }
+}
diff --git a/cli/compat/testdata/basic_deps/package.json b/cli/compat/testdata/basic_deps/package.json
new file mode 100644
index 000000000..138d401ed
--- /dev/null
+++ b/cli/compat/testdata/basic_deps/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "main_program",
+ "type": "module",
+ "dependencies": {
+ "foo": "1.0.0"
+ }
+}
diff --git a/cli/compat/testdata/conditions/main.js b/cli/compat/testdata/conditions/main.js
new file mode 100644
index 000000000..cafddb5d6
--- /dev/null
+++ b/cli/compat/testdata/conditions/main.js
@@ -0,0 +1 @@
+import "imports_exports";
diff --git a/cli/compat/testdata/conditions/node_modules/imports_exports/import_export.js b/cli/compat/testdata/conditions/node_modules/imports_exports/import_export.js
new file mode 100644
index 000000000..3ebd222ea
--- /dev/null
+++ b/cli/compat/testdata/conditions/node_modules/imports_exports/import_export.js
@@ -0,0 +1,6 @@
+import dep from "#dep";
+
+export default {
+ bar: "bar",
+ dep,
+};
diff --git a/cli/compat/testdata/conditions/node_modules/imports_exports/import_polyfill.js b/cli/compat/testdata/conditions/node_modules/imports_exports/import_polyfill.js
new file mode 100644
index 000000000..76716a3ef
--- /dev/null
+++ b/cli/compat/testdata/conditions/node_modules/imports_exports/import_polyfill.js
@@ -0,0 +1,3 @@
+export default {
+ polyfill: "import",
+};
diff --git a/cli/compat/testdata/conditions/node_modules/imports_exports/package.json b/cli/compat/testdata/conditions/node_modules/imports_exports/package.json
new file mode 100644
index 000000000..5d26359db
--- /dev/null
+++ b/cli/compat/testdata/conditions/node_modules/imports_exports/package.json
@@ -0,0 +1,17 @@
+{
+ "version": "1.0.0",
+ "name": "imports_exports",
+ "main": "./require_export.cjs",
+ "imports": {
+ "#dep": {
+ "import": "./import_polyfill.js",
+ "require": "./require_polyfill.js"
+ }
+ },
+ "exports": {
+ ".": {
+ "import": "./import_export.js",
+ "require": "./require_export.cjs"
+ }
+ }
+}
diff --git a/cli/compat/testdata/conditions/node_modules/imports_exports/require_export.cjs b/cli/compat/testdata/conditions/node_modules/imports_exports/require_export.cjs
new file mode 100644
index 000000000..11648c0d7
--- /dev/null
+++ b/cli/compat/testdata/conditions/node_modules/imports_exports/require_export.cjs
@@ -0,0 +1,6 @@
+const dep = require("#dep");
+
+module.exports = {
+ foo: "foo",
+ dep,
+}; \ No newline at end of file
diff --git a/cli/compat/testdata/conditions/node_modules/imports_exports/require_polyfill.js b/cli/compat/testdata/conditions/node_modules/imports_exports/require_polyfill.js
new file mode 100644
index 000000000..1023fd65c
--- /dev/null
+++ b/cli/compat/testdata/conditions/node_modules/imports_exports/require_polyfill.js
@@ -0,0 +1,3 @@
+module.exports = {
+ polyfill: "require",
+};
diff --git a/cli/compat/testdata/conditions/package.json b/cli/compat/testdata/conditions/package.json
new file mode 100644
index 000000000..4d6d004dd
--- /dev/null
+++ b/cli/compat/testdata/conditions/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "conditions",
+ "type": "module",
+ "dependencies": {
+ "imports_exports": "1.0.0"
+ }
+}
diff --git a/cli/compat/testdata/deep/a/b/c/d/main.js b/cli/compat/testdata/deep/a/b/c/d/main.js
new file mode 100644
index 000000000..c0748305d
--- /dev/null
+++ b/cli/compat/testdata/deep/a/b/c/d/main.js
@@ -0,0 +1 @@
+import "foo";
diff --git a/cli/compat/testdata/deep/node_modules/foo/index.js b/cli/compat/testdata/deep/node_modules/foo/index.js
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/cli/compat/testdata/deep/node_modules/foo/index.js
diff --git a/cli/compat/testdata/deep/node_modules/foo/package.json b/cli/compat/testdata/deep/node_modules/foo/package.json
new file mode 100644
index 000000000..a74d52fd3
--- /dev/null
+++ b/cli/compat/testdata/deep/node_modules/foo/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "foo",
+ "type": "module",
+ "exports": "./index.js"
+}
diff --git a/cli/main.rs b/cli/main.rs
index 8a85cacfb..9dd7a354e 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -1092,6 +1092,11 @@ async fn run_command(
return run_with_watch(flags, run_flags.script).await;
}
+ // TODO(bartlomieju): it should not be resolved here if we're in compat mode
+ // because it might be a bare specifier
+ // TODO(bartlomieju): actually I think it will also fail if there's an import
+ // map specified and bare specifier is used on the command line - this should
+ // probably call `ProcState::resolve` instead
let main_module = resolve_url_or_path(&run_flags.script)?;
let ps = ProcState::build(flags.clone()).await?;
let permissions = Permissions::from_options(&flags.clone().into());
@@ -1114,10 +1119,41 @@ async fn run_command(
};
debug!("main_module {}", main_module);
+
if flags.compat {
+ // TODO(bartlomieju): fix me
+ assert_eq!(main_module.scheme(), "file");
+
+ // Set up Node globals
worker.execute_side_module(&compat::GLOBAL_URL).await?;
+ // And `module` module that we'll use for checking which
+ // loader to use and potentially load CJS module with.
+ // This allows to skip permission check for `--allow-net`
+ // which would otherwise be requested by dynamically importing
+ // this file.
+ worker.execute_side_module(&compat::MODULE_URL).await?;
+
+ let use_esm_loader = compat::check_if_should_use_esm_loader(
+ &mut worker.js_runtime,
+ &main_module.to_file_path().unwrap().display().to_string(),
+ )
+ .await?;
+
+ if use_esm_loader {
+ // ES module execution in Node compatiblity mode
+ worker.execute_main_module(&main_module).await?;
+ } else {
+ // CJS module execution in Node compatiblity mode
+ compat::load_cjs_module(
+ &mut worker.js_runtime,
+ &main_module.to_file_path().unwrap().display().to_string(),
+ )?;
+ }
+ } else {
+ // Regular ES module execution
+ worker.execute_main_module(&main_module).await?;
}
- worker.execute_main_module(&main_module).await?;
+
worker.execute_script(
&located_script_name!(),
"window.dispatchEvent(new Event('load'))",
diff --git a/cli/proc_state.rs b/cli/proc_state.rs
index e45e7c539..461599fc4 100644
--- a/cli/proc_state.rs
+++ b/cli/proc_state.rs
@@ -3,6 +3,7 @@
use crate::cache;
use crate::colors;
use crate::compat;
+use crate::compat::NodeEsmResolver;
use crate::config_file::ConfigFile;
use crate::deno_dir;
use crate::emit;
@@ -195,7 +196,7 @@ impl ProcState {
None
};
- let mut maybe_import_map: Option<ImportMap> =
+ let maybe_import_map: Option<ImportMap> =
match flags.import_map_path.as_ref() {
None => None,
Some(import_map_url) => {
@@ -217,32 +218,6 @@ impl ProcState {
}
};
- if flags.compat {
- let mut import_map = match maybe_import_map {
- Some(import_map) => import_map,
- None => {
- // INFO: we're creating an empty import map, with its specifier pointing
- // to `CWD/node_import_map.json` to make sure the map still works as expected.
- let import_map_specifier =
- std::env::current_dir()?.join("node_import_map.json");
- ImportMap::from_json(import_map_specifier.to_str().unwrap(), "{}")
- .unwrap()
- }
- };
- let node_builtins = compat::get_mapped_node_builtins();
- let diagnostics = import_map.update_imports(node_builtins)?;
-
- if !diagnostics.is_empty() {
- log::info!("Some Node built-ins were not added to the import map:");
- for diagnostic in diagnostics {
- log::info!(" - {}", diagnostic);
- }
- log::info!("If you want to use Node built-ins provided by Deno remove listed specifiers from \"imports\" mapping in the import map file.");
- }
-
- maybe_import_map = Some(import_map);
- }
-
let maybe_inspect_host = flags.inspect.or(flags.inspect_brk);
let maybe_inspector_server = maybe_inspect_host.map(|host| {
Arc::new(InspectorServer::new(host, version::get_user_agent()))
@@ -316,14 +291,29 @@ impl ProcState {
);
let maybe_locker = as_maybe_locker(self.lockfile.clone());
let maybe_imports = self.get_maybe_imports();
- let maybe_resolver =
+ let node_resolver = NodeEsmResolver;
+ let import_map_resolver =
self.maybe_import_map.as_ref().map(ImportMapResolver::new);
+ let maybe_resolver = if self.flags.compat {
+ Some(node_resolver.as_resolver())
+ } else {
+ import_map_resolver.as_ref().map(|im| im.as_resolver())
+ };
+ // TODO(bartlomieju): this is very make-shift, is there an existing API
+ // that we could include it like with "maybe_imports"?
+ let roots = if self.flags.compat {
+ let mut r = vec![compat::GLOBAL_URL.clone()];
+ r.extend(roots);
+ r
+ } else {
+ roots
+ };
let graph = deno_graph::create_graph(
roots,
is_dynamic,
maybe_imports,
&mut cache,
- maybe_resolver.as_ref().map(|im| im.as_resolver()),
+ maybe_resolver,
maybe_locker,
None,
)
diff --git a/cli/tests/integration/compat_tests.rs b/cli/tests/integration/compat_tests.rs
index 6b6ab81b5..17388a78e 100644
--- a/cli/tests/integration/compat_tests.rs
+++ b/cli/tests/integration/compat_tests.rs
@@ -9,21 +9,15 @@ itest!(globals {
});
itest!(fs_promises {
- args: "run --compat --unstable -A compat/fs_promises.js",
+ args: "run --compat --unstable -A compat/fs_promises.mjs",
output: "compat/fs_promises.out",
});
itest!(node_prefix_fs_promises {
- args: "run --compat --unstable -A compat/node_fs_promises.js",
+ args: "run --compat --unstable -A compat/node_fs_promises.mjs",
output: "compat/fs_promises.out",
});
-itest!(existing_import_map {
- args: "run --compat --unstable --import-map compat/existing_import_map.json compat/fs_promises.js",
- output: "compat/existing_import_map.out",
- exit_code: 1,
-});
-
#[test]
fn globals_in_repl() {
let (out, _err) = util::run_and_collect_output_with_args(
diff --git a/cli/tests/testdata/compat/existing_import_map.json b/cli/tests/testdata/compat/existing_import_map.json
deleted file mode 100644
index db59c0cc2..000000000
--- a/cli/tests/testdata/compat/existing_import_map.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "imports": {
- "fs/promises": "./non_existent_file.js"
- }
-}
diff --git a/cli/tests/testdata/compat/existing_import_map.out b/cli/tests/testdata/compat/existing_import_map.out
deleted file mode 100644
index 46125d411..000000000
--- a/cli/tests/testdata/compat/existing_import_map.out
+++ /dev/null
@@ -1,7 +0,0 @@
-[WILDCARD]
-Some Node built-ins were not added to the import map:
- - "fs/promises" already exists and is mapped to "file://[WILDCARD]/non_existent_file.js"
-If you want to use Node built-ins provided by Deno remove listed specifiers from "imports" mapping in the import map file.
-[WILDCARD]
-error: Cannot load module "file://[WILDCARD]/non_existent_file.js".
- at file://[WILDCARD]/fs_promises.js:1:16
diff --git a/cli/tests/testdata/compat/fs_promises.js b/cli/tests/testdata/compat/fs_promises.mjs
index 3f7b4c935..3f7b4c935 100644
--- a/cli/tests/testdata/compat/fs_promises.js
+++ b/cli/tests/testdata/compat/fs_promises.mjs
diff --git a/cli/tests/testdata/compat/globals.out b/cli/tests/testdata/compat/globals.out
index 0bc09137b..32230fda3 100644
--- a/cli/tests/testdata/compat/globals.out
+++ b/cli/tests/testdata/compat/globals.out
@@ -2,6 +2,8 @@
process {
[WILDCARD]
}
-[Function: Buffer]
+[Function: Buffer] {
+[WILDCARD]
+}
[Function: setImmediate]
[Function: clearTimeout]
diff --git a/cli/tests/testdata/compat/node_fs_promises.js b/cli/tests/testdata/compat/node_fs_promises.mjs
index b2174b22b..b2174b22b 100644
--- a/cli/tests/testdata/compat/node_fs_promises.js
+++ b/cli/tests/testdata/compat/node_fs_promises.mjs