summaryrefslogtreecommitdiff
path: root/cli/node
diff options
context:
space:
mode:
Diffstat (limited to 'cli/node')
-rw-r--r--cli/node/analyze.rs161
-rw-r--r--cli/node/mod.rs730
2 files changed, 891 insertions, 0 deletions
diff --git a/cli/node/analyze.rs b/cli/node/analyze.rs
new file mode 100644
index 000000000..9da2cbf4b
--- /dev/null
+++ b/cli/node/analyze.rs
@@ -0,0 +1,161 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::collections::HashSet;
+
+use deno_ast::swc::common::SyntaxContext;
+use deno_ast::view::Node;
+use deno_ast::view::NodeTrait;
+use deno_ast::ModuleSpecifier;
+use deno_ast::ParsedSource;
+use deno_ast::SourceRanged;
+use deno_core::error::AnyError;
+use std::fmt::Write;
+
+static NODE_GLOBALS: &[&str] = &[
+ "Buffer",
+ "clearImmediate",
+ "clearInterval",
+ "clearTimeout",
+ "global",
+ "process",
+ "setImmediate",
+ "setInterval",
+ "setTimeout",
+];
+
+// TODO(dsherret): this code is way more inefficient than it needs to be.
+//
+// In the future, we should disable capturing tokens & scope analysis
+// and instead only use swc's APIs to go through the portions of the tree
+// that we know will affect the global scope while still ensuring that
+// `var` decls are taken into consideration.
+
+pub fn esm_code_with_node_globals(
+ specifier: &ModuleSpecifier,
+ code: String,
+) -> Result<String, AnyError> {
+ let parsed_source = deno_ast::parse_program(deno_ast::ParseParams {
+ specifier: specifier.to_string(),
+ text_info: deno_ast::SourceTextInfo::from_string(code),
+ media_type: deno_ast::MediaType::from(specifier),
+ capture_tokens: true,
+ scope_analysis: true,
+ maybe_syntax: None,
+ })?;
+ let top_level_decls = analyze_top_level_decls(&parsed_source)?;
+ let mut globals = Vec::with_capacity(NODE_GLOBALS.len());
+ let has_global_this = top_level_decls.contains("globalThis");
+ for global in NODE_GLOBALS.iter() {
+ if !top_level_decls.contains(&global.to_string()) {
+ globals.push(*global);
+ }
+ }
+
+ let mut result = String::new();
+ let has_deno_decl = top_level_decls.contains("Deno");
+ let global_this_expr = if has_deno_decl {
+ if top_level_decls.contains("window") {
+ // Will probably never happen, but if it does then we should consider
+ // creating an obscure global name to get this from.
+ panic!("The node esm module had a local `Deno` declaration and `window` declaration.");
+ }
+ // fallback to using `window.Deno`
+ "window.Deno[Deno.internal].node.globalThis"
+ } else {
+ "Deno[Deno.internal].node.globalThis"
+ };
+ let global_this_expr = if has_global_this {
+ global_this_expr
+ } else {
+ write!(result, "var globalThis = {};", global_this_expr).unwrap();
+ "globalThis"
+ };
+ for global in globals {
+ write!(result, "var {0} = {1}.{0};", global, global_this_expr).unwrap();
+ }
+
+ result.push_str(parsed_source.text_info().text_str());
+
+ Ok(result)
+}
+
+fn analyze_top_level_decls(
+ parsed_source: &ParsedSource,
+) -> Result<HashSet<String>, AnyError> {
+ let top_level_context = parsed_source.top_level_context();
+
+ parsed_source.with_view(|program| {
+ let mut results = HashSet::new();
+ visit_children(program.into(), top_level_context, &mut results);
+ Ok(results)
+ })
+}
+
+fn visit_children(
+ node: Node,
+ top_level_context: SyntaxContext,
+ results: &mut HashSet<String>,
+) {
+ if let Node::Ident(ident) = node {
+ if ident.ctxt() == top_level_context && is_local_declaration_ident(node) {
+ results.insert(ident.sym().to_string());
+ }
+ }
+
+ for child in node.children() {
+ visit_children(child, top_level_context, results);
+ }
+}
+
+fn is_local_declaration_ident(node: Node) -> bool {
+ if let Some(parent) = node.parent() {
+ match parent {
+ Node::BindingIdent(decl) => decl.id.range().contains(&node.range()),
+ Node::ClassDecl(decl) => decl.ident.range().contains(&node.range()),
+ Node::ClassExpr(decl) => decl
+ .ident
+ .as_ref()
+ .map(|i| i.range().contains(&node.range()))
+ .unwrap_or(false),
+ Node::TsInterfaceDecl(decl) => decl.id.range().contains(&node.range()),
+ Node::FnDecl(decl) => decl.ident.range().contains(&node.range()),
+ Node::FnExpr(decl) => decl
+ .ident
+ .as_ref()
+ .map(|i| i.range().contains(&node.range()))
+ .unwrap_or(false),
+ Node::TsModuleDecl(decl) => decl.id.range().contains(&node.range()),
+ Node::TsNamespaceDecl(decl) => decl.id.range().contains(&node.range()),
+ Node::VarDeclarator(decl) => decl.name.range().contains(&node.range()),
+ Node::ImportNamedSpecifier(decl) => {
+ decl.local.range().contains(&node.range())
+ }
+ Node::ImportDefaultSpecifier(decl) => {
+ decl.local.range().contains(&node.range())
+ }
+ Node::ImportStarAsSpecifier(decl) => decl.range().contains(&node.range()),
+ Node::KeyValuePatProp(decl) => decl.key.range().contains(&node.range()),
+ Node::AssignPatProp(decl) => decl.key.range().contains(&node.range()),
+ _ => false,
+ }
+ } else {
+ false
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_esm_code_with_node_globals() {
+ let r = esm_code_with_node_globals(
+ &ModuleSpecifier::parse("https://example.com/foo/bar.js").unwrap(),
+ "export const x = 1;".to_string(),
+ )
+ .unwrap();
+ assert!(r.contains("var globalThis = Deno[Deno.internal].node.globalThis;"));
+ assert!(r.contains("var process = globalThis.process;"));
+ assert!(r.contains("export const x = 1;"));
+ }
+}
diff --git a/cli/node/mod.rs b/cli/node/mod.rs
new file mode 100644
index 000000000..2af4f2308
--- /dev/null
+++ b/cli/node/mod.rs
@@ -0,0 +1,730 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::collections::HashSet;
+use std::path::Path;
+use std::path::PathBuf;
+
+use deno_ast::MediaType;
+use deno_ast::ModuleSpecifier;
+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::Map;
+use deno_core::serde_json::Value;
+use deno_core::url::Url;
+use deno_core::JsRuntime;
+use deno_graph::source::ResolveResponse;
+use deno_runtime::deno_node::get_package_scope_config;
+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::DenoDirNpmResolver;
+use deno_runtime::deno_node::PackageJson;
+use deno_runtime::deno_node::DEFAULT_CONDITIONS;
+use once_cell::sync::Lazy;
+use path_clean::PathClean;
+use regex::Regex;
+
+use crate::compat;
+use crate::file_fetcher::FileFetcher;
+use crate::npm::GlobalNpmPackageResolver;
+use crate::npm::NpmPackageReference;
+use crate::npm::NpmPackageResolver;
+
+mod analyze;
+
+pub use analyze::esm_code_with_node_globals;
+
+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) {{
+ const moduleAll = await import(moduleAllUrl);
+ Deno[Deno.internal].node.initialize(moduleAll.default);
+ }})('{}');"#,
+ compat::MODULE_ALL_URL.as_str(),
+ );
+
+ 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,
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<Option<ResolveResponse>, AnyError> {
+ // TODO(bartlomieju): skipped "policy" part as we don't plan to support it
+
+ if let Some(resolved) = compat::try_resolve_builtin_module(specifier) {
+ return Ok(Some(ResolveResponse::Esm(resolved)));
+ }
+
+ if let Ok(url) = Url::parse(specifier) {
+ if url.scheme() == "data" {
+ return Ok(Some(ResolveResponse::Specifier(url)));
+ }
+
+ let protocol = url.scheme();
+
+ if protocol == "node" {
+ let split_specifier = url.as_str().split(':');
+ let specifier = split_specifier.skip(1).collect::<String>();
+ if let Some(resolved) = compat::try_resolve_builtin_module(&specifier) {
+ return Ok(Some(ResolveResponse::Esm(resolved)));
+ } else {
+ return Err(generic_error(format!("Unknown module {}", specifier)));
+ }
+ }
+
+ if protocol != "file" && protocol != "data" {
+ return Err(compat::errors::err_unsupported_esm_url_scheme(&url));
+ }
+
+ // todo(THIS PR): I think this is handled upstream so can be removed?
+ if referrer.scheme() == "data" {
+ let url = referrer.join(specifier).map_err(AnyError::from)?;
+ return Ok(Some(ResolveResponse::Specifier(url)));
+ }
+ }
+
+ let conditions = DEFAULT_CONDITIONS;
+ let url = module_resolve(specifier, referrer, conditions, npm_resolver)?;
+ let url = match url {
+ Some(url) => url,
+ None => return Ok(None),
+ };
+
+ let resolve_response = url_to_resolve_response(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,
+ npm_resolver: &GlobalNpmPackageResolver,
+) -> Result<Option<ResolveResponse>, AnyError> {
+ let package_folder = npm_resolver
+ .resolve_package_from_deno_module(&reference.req)?
+ .folder_path;
+ let maybe_url = package_config_resolve(
+ reference.sub_path.as_deref().unwrap_or("."),
+ &package_folder,
+ npm_resolver,
+ )
+ .map(Some)
+ .with_context(|| {
+ format!("Error resolving package config for '{}'.", reference)
+ })?;
+ let url = match maybe_url {
+ Some(url) => url,
+ None => return Ok(None),
+ };
+
+ let resolve_response = url_to_resolve_response(url, npm_resolver)?;
+ // TODO(bartlomieju): skipped checking errors for commonJS resolution and
+ // "preserveSymlinksMain"/"preserveSymlinks" options.
+ Ok(Some(resolve_response))
+}
+
+fn package_config_resolve(
+ package_subpath: &str,
+ package_dir: &Path,
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<ModuleSpecifier, AnyError> {
+ let package_json_path = package_dir.join("package.json");
+ // todo(dsherret): remove base from this code
+ let base =
+ ModuleSpecifier::from_directory_path(package_json_path.parent().unwrap())
+ .unwrap();
+ let package_config =
+ PackageJson::load(npm_resolver, package_json_path.clone())?;
+ let package_json_url =
+ ModuleSpecifier::from_file_path(&package_json_path).unwrap();
+ if let Some(exports) = &package_config.exports {
+ return package_exports_resolve(
+ package_json_url,
+ package_subpath.to_string(),
+ exports,
+ &base,
+ DEFAULT_CONDITIONS,
+ npm_resolver,
+ );
+ }
+ if package_subpath == "." {
+ return legacy_main_resolve(&package_json_url, &package_config, &base);
+ }
+
+ package_json_url
+ .join(package_subpath)
+ .map_err(AnyError::from)
+}
+
+fn url_to_resolve_response(
+ url: ModuleSpecifier,
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> Result<ResolveResponse, AnyError> {
+ Ok(if url.as_str().starts_with("http") {
+ ResolveResponse::Esm(url)
+ } else if url.as_str().ends_with(".js") {
+ let package_config = get_package_scope_config(&url, npm_resolver)?;
+ if package_config.typ == "module" {
+ ResolveResponse::Esm(url)
+ } else {
+ ResolveResponse::CommonJs(url)
+ }
+ } else if url.as_str().ends_with(".cjs") {
+ ResolveResponse::CommonJs(url)
+ } else {
+ ResolveResponse::Esm(url)
+ })
+}
+
+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);
+ }
+
+ // todo(dsherret): cache
+ let encoded_sep_re = Regex::new(r"%2F|%2C").unwrap();
+
+ if encoded_sep_re.is_match(resolved.path()) {
+ return Err(compat::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(compat::errors::err_unsupported_dir_import(
+ resolved.as_str(),
+ base.as_str(),
+ ));
+ } else if !is_file {
+ return Err(compat::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 DenoDirNpmResolver,
+) -> Result<Option<ModuleSpecifier>, AnyError> {
+ let url = if should_be_treated_as_relative_or_absolute_path(specifier) {
+ let resolved_specifier = referrer.join(specifier)?;
+ Some(resolved_specifier)
+ } else if specifier.starts_with('#') {
+ Some(package_imports_resolve(
+ specifier,
+ referrer,
+ conditions,
+ npm_resolver,
+ )?)
+ } else if let Ok(resolved) = Url::parse(specifier) {
+ Some(resolved)
+ } else {
+ Some(package_resolve(
+ specifier,
+ referrer,
+ conditions,
+ npm_resolver,
+ )?)
+ };
+ Ok(match url {
+ Some(url) => Some(finalize_resolution(url, referrer)?),
+ None => None,
+ })
+}
+
+fn add_export(source: &mut Vec<String>, name: &str, initializer: &str) {
+ // 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) {
+ // we can't create an identifier with a reserved word, 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_reexport_temp__{} = {};",
+ name, initializer
+ ));
+ source.push(format!(
+ "export {{ __deno_reexport_temp__{0} as \"{0}\" }};",
+ 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: &GlobalNpmPackageResolver,
+) -> Result<String, AnyError> {
+ 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();
+
+ let mut source = vec![
+ r#"const require = Deno[Deno.internal].require.Module.createRequire(import.meta.url);"#.to_string(),
+ ];
+
+ // if there are reexports, handle them first
+ for (idx, reexport) in analysis.reexports.iter().enumerate() {
+ // Firstly, resolve relate reexport specifier
+ // todo(dsherret): call module_resolve instead?
+ let resolved_reexport = resolve(
+ reexport,
+ specifier,
+ // 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();
+ // Secondly, read the source code from disk
+ let reexport_file = file_fetcher.get_source(&reexport_specifier).unwrap();
+ // Now perform analysis again
+ {
+ let parsed_source = deno_ast::parse_script(deno_ast::ParseParams {
+ specifier: reexport_specifier.to_string(),
+ text_info: deno_ast::SourceTextInfo::new(reexport_file.source),
+ media_type: reexport_file.media_type,
+ capture_tokens: true,
+ scope_analysis: false,
+ maybe_syntax: None,
+ })?;
+ let analysis = parsed_source.analyze_cjs();
+
+ source.push(format!(
+ "const reexport{} = require(\"{}\");",
+ idx, reexport
+ ));
+
+ for export in analysis.exports.iter().filter(|e| e.as_str() != "default")
+ {
+ add_export(&mut source, export, &format!("Deno[Deno.internal].require.bindExport(reexport{0}.{1}, reexport{0})", idx, export));
+ }
+ }
+ }
+
+ source.push(format!(
+ "const mod = require(\"{}\");",
+ specifier
+ .to_file_path()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .replace('\\', "\\\\")
+ .replace('\'', "\\\'")
+ .replace('\"', "\\\"")
+ ));
+
+ let mut had_default = false;
+ for export in analysis.exports.iter() {
+ if export.as_str() == "default" {
+ // todo(dsherret): we should only do this if there was a `_esModule: true` instead
+ source.push(format!(
+ "export default Deno[Deno.internal].require.bindExport(mod.{}, mod);",
+ export,
+ ));
+ had_default = true;
+ } else {
+ add_export(
+ &mut source,
+ export,
+ &format!(
+ "Deno[Deno.internal].require.bindExport(mod.{}, mod)",
+ export
+ ),
+ );
+ }
+ }
+
+ if !had_default {
+ source.push("export default mod;".to_string());
+ }
+
+ let translated_source = source.join("\n");
+ Ok(translated_source)
+}
+
+fn resolve_package_target_string(
+ target: &str,
+ subpath: Option<String>,
+) -> String {
+ if let Some(subpath) = subpath {
+ target.replace('*', &subpath)
+ } else {
+ target.to_string()
+ }
+}
+
+fn resolve(
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ conditions: &[&str],
+ npm_resolver: &dyn DenoDirNpmResolver,
+) -> 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_subpath) = parse_specifier(specifier).unwrap();
+
+ // todo(dsherret): use not_found error on not found here
+ let module_dir =
+ npm_resolver.resolve_package_folder_from_path(&referrer_path)?;
+
+ let package_json_path = module_dir.join("package.json");
+ if package_json_path.exists() {
+ let package_json = PackageJson::load(npm_resolver, package_json_path)?;
+
+ if let Some(map) = package_json.exports {
+ if let Some((key, subpath)) = exports_resolve(&map, &package_subpath) {
+ let value = map.get(&key).unwrap();
+ let s = conditions_resolve(value, conditions);
+
+ let t = resolve_package_target_string(&s, subpath);
+ return Ok(module_dir.join(t).clean());
+ } else {
+ todo!()
+ }
+ }
+
+ // old school
+ if package_subpath != "." {
+ let d = module_dir.join(package_subpath);
+ if let Ok(m) = d.metadata() {
+ if m.is_dir() {
+ return Ok(d.join("index.js").clean());
+ }
+ }
+ return file_extension_probe(d, &referrer_path);
+ } else if let Some(main) = package_json.main {
+ return Ok(module_dir.join(main).clean());
+ } else {
+ return Ok(module_dir.join("index.js").clean());
+ }
+ }
+
+ Err(not_found(specifier, &referrer_path))
+}
+
+fn conditions_resolve(value: &Value, conditions: &[&str]) -> String {
+ match value {
+ Value::String(s) => s.to_string(),
+ Value::Object(map) => {
+ for condition in conditions {
+ if let Some(x) = map.get(&condition.to_string()) {
+ if let Value::String(s) = x {
+ return s.to_string();
+ } else {
+ todo!()
+ }
+ }
+ }
+ todo!()
+ }
+ _ => todo!(),
+ }
+}
+
+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('/');
+ } 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 exports_resolve(
+ map: &Map<String, Value>,
+ subpath: &str,
+) -> Option<(String, Option<String>)> {
+ if map.contains_key(subpath) {
+ return Some((subpath.to_string(), None));
+ }
+
+ // best match
+ let mut best_match = None;
+ for key in map.keys() {
+ if let Some(pattern_index) = key.find('*') {
+ let key_sub = &key[0..pattern_index];
+ if subpath.starts_with(key_sub) {
+ if subpath.ends_with('/') {
+ todo!()
+ }
+ let pattern_trailer = &key[pattern_index + 1..];
+
+ if subpath.len() > key.len()
+ && subpath.ends_with(pattern_trailer)
+ // && pattern_key_compare(best_match, key) == 1
+ && key.rfind('*') == Some(pattern_index)
+ {
+ let rest = subpath
+ [pattern_index..(subpath.len() - pattern_trailer.len())]
+ .to_string();
+ best_match = Some((key, rest));
+ }
+ }
+ }
+ }
+
+ if let Some((key, subpath_)) = best_match {
+ return Some((key.to_string(), Some(subpath_)));
+ }
+
+ None
+}
+
+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(
+ mut p: PathBuf,
+ referrer: &Path,
+) -> Result<PathBuf, AnyError> {
+ if p.exists() && !p.is_dir() {
+ Ok(p.clean())
+ } else {
+ p.set_extension("js");
+ if p.exists() && !p.is_dir() {
+ Ok(p)
+ } else {
+ Err(not_found(&p.clean().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 super::*;
+
+ #[test]
+ fn test_add_export() {
+ let mut source = vec![];
+
+ let exports = vec!["static", "server", "app"];
+ for export in exports {
+ add_export(&mut source, export, "init");
+ }
+ assert_eq!(
+ source,
+ vec![
+ "const __deno_reexport_temp__static = init;".to_string(),
+ "export { __deno_reexport_temp__static as \"static\" };".to_string(),
+ "export const server = init;".to_string(),
+ "export const app = init;".to_string(),
+ ]
+ )
+ }
+
+ #[test]
+ fn test_resolve_package_target_string() {
+ assert_eq!(resolve_package_target_string("foo", None), "foo");
+ assert_eq!(
+ resolve_package_target_string("*foo", Some("bar".to_string())),
+ "barfoo"
+ );
+ }
+}