Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(npm): support bare specifiers in package.json having a path #17903

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ use deno_runtime::deno_tls::webpki_roots;
use deno_runtime::inspector_server::InspectorServer;
use deno_runtime::permissions::PermissionsOptions;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::env;
use std::io::BufReader;
use std::io::Cursor;
Expand Down Expand Up @@ -799,7 +799,7 @@ impl CliOptions {

pub fn maybe_package_json_deps(
&self,
) -> Result<Option<HashMap<String, NpmPackageReq>>, AnyError> {
) -> Result<Option<BTreeMap<String, NpmPackageReq>>, AnyError> {
if matches!(
self.flags.subcommand,
DenoSubcommand::Task(TaskFlags { task: None, .. })
Expand Down
11 changes: 5 additions & 6 deletions cli/args/package_json.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
Expand Down Expand Up @@ -36,10 +37,10 @@ pub fn parse_dep_entry_name_and_raw_version<'a>(
/// entries to npm specifiers which can then be used in the resolver.
pub fn get_local_package_json_version_reqs(
package_json: &PackageJson,
) -> Result<HashMap<String, NpmPackageReq>, AnyError> {
) -> Result<BTreeMap<String, NpmPackageReq>, AnyError> {
fn insert_deps(
deps: Option<&HashMap<String, String>>,
result: &mut HashMap<String, NpmPackageReq>,
result: &mut BTreeMap<String, NpmPackageReq>,
) -> Result<(), AnyError> {
if let Some(deps) = deps {
for (key, value) in deps {
Expand Down Expand Up @@ -73,9 +74,7 @@ pub fn get_local_package_json_version_reqs(

let deps = package_json.dependencies.as_ref();
let dev_deps = package_json.dev_dependencies.as_ref();
let mut result = HashMap::with_capacity(
deps.map(|d| d.len()).unwrap_or(0) + dev_deps.map(|d| d.len()).unwrap_or(0),
);
let mut result = BTreeMap::new();

// insert the dev dependencies first so the dependencies will
// take priority and overwrite any collisions
Expand Down Expand Up @@ -166,7 +165,7 @@ mod test {
let result = get_local_package_json_version_reqs(&package_json).unwrap();
assert_eq!(
result,
HashMap::from([
BTreeMap::from([
(
"test".to_string(),
NpmPackageReq::from_str("test@^1.2").unwrap()
Expand Down
3 changes: 1 addition & 2 deletions cli/lsp/documents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,7 @@ impl Documents {
fn calculate_resolver_config_hash(
maybe_import_map: Option<&import_map::ImportMap>,
maybe_jsx_config: Option<&JsxImportSourceConfig>,
maybe_package_json_deps: Option<&HashMap<String, NpmPackageReq>>,
maybe_package_json_deps: Option<&BTreeMap<String, NpmPackageReq>>,
) -> u64 {
let mut hasher = FastInsecureHasher::default();
if let Some(import_map) = maybe_import_map {
Expand All @@ -1181,7 +1181,6 @@ impl Documents {
hasher.write_hashable(&jsx_config);
}
if let Some(deps) = maybe_package_json_deps {
let deps = deps.iter().collect::<BTreeMap<_, _>>();
hasher.write_hashable(&deps);
}
hasher.finish()
Expand Down
4 changes: 2 additions & 2 deletions cli/npm/resolvers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use deno_runtime::deno_node::RequireNpmResolver;
use global::GlobalNpmPackageResolver;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
Expand Down Expand Up @@ -227,7 +227,7 @@ impl NpmPackageResolver {
/// Adds the package reqs from a package.json if they exist.
pub async fn add_package_json_deps(
&self,
maybe_package_json_deps: Option<&HashMap<String, NpmPackageReq>>,
maybe_package_json_deps: Option<&BTreeMap<String, NpmPackageReq>>,
) -> Result<(), AnyError> {
if let Some(deps) = maybe_package_json_deps {
let mut package_reqs = deps.values().cloned().collect::<Vec<_>>();
Expand Down
89 changes: 86 additions & 3 deletions cli/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use deno_graph::source::UnknownBuiltInNodeModuleError;
use deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE;
use deno_runtime::deno_node::is_builtin_node_module;
use import_map::ImportMap;
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::sync::Arc;

use crate::args::JsxImportSourceConfig;
Expand All @@ -26,7 +26,7 @@ use crate::npm::NpmResolution;
#[derive(Debug, Clone)]
pub struct CliGraphResolver {
maybe_import_map: Option<Arc<ImportMap>>,
maybe_package_json_deps: Option<HashMap<String, NpmPackageReq>>,
maybe_package_json_deps: Option<BTreeMap<String, NpmPackageReq>>,
maybe_default_jsx_import_source: Option<String>,
maybe_jsx_import_source_module: Option<String>,
no_npm: bool,
Expand Down Expand Up @@ -62,7 +62,7 @@ impl CliGraphResolver {
no_npm: bool,
npm_registry_api: NpmRegistryApi,
npm_resolution: NpmResolution,
maybe_package_json_deps: Option<HashMap<String, NpmPackageReq>>,
maybe_package_json_deps: Option<BTreeMap<String, NpmPackageReq>>,
) -> Self {
Self {
maybe_import_map,
Expand Down Expand Up @@ -120,6 +120,9 @@ impl Resolver for CliGraphResolver {
}

if let Some(deps) = self.maybe_package_json_deps.as_ref() {
if let Some(specifier) = resolve_package_json_dep(specifier, deps)? {
return Ok(specifier);
}
if let Some(req) = deps.get(specifier) {
return Ok(ModuleSpecifier::parse(&format!("npm:{req}")).unwrap());
}
Expand All @@ -129,6 +132,25 @@ impl Resolver for CliGraphResolver {
}
}

fn resolve_package_json_dep(
specifier: &str,
deps: &BTreeMap<String, NpmPackageReq>,
) -> Result<Option<ModuleSpecifier>, deno_core::url::ParseError> {
for (bare_specifier, req) in deps {
if specifier.starts_with(bare_specifier) {
if specifier.len() == bare_specifier.len() {
return ModuleSpecifier::parse(&format!("npm:{req}")).map(Some);
}
let path = &specifier[bare_specifier.len()..];
if path.starts_with('/') {
return ModuleSpecifier::parse(&format!("npm:/{req}{path}")).map(Some);
}
}
}

Ok(None)
}

impl NpmResolver for CliGraphResolver {
fn resolve_builtin_node_module(
&self,
Expand Down Expand Up @@ -184,3 +206,64 @@ impl NpmResolver for CliGraphResolver {
.resolve_package_req_for_deno_graph(package_req)
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_resolve_package_json_dep() {
fn resolve(
specifier: &str,
deps: &BTreeMap<String, NpmPackageReq>,
) -> Result<Option<String>, String> {
resolve_package_json_dep(specifier, deps)
.map(|s| s.map(|s| s.to_string()))
.map_err(|err| err.to_string())
}

let deps = BTreeMap::from([
(
"package".to_string(),
NpmPackageReq::from_str("package@1.0").unwrap(),
),
(
"package-alias".to_string(),
NpmPackageReq::from_str("package@^1.2").unwrap(),
),
(
"@deno/test".to_string(),
NpmPackageReq::from_str("@deno/test@~0.2").unwrap(),
),
]);

assert_eq!(
resolve("package", &deps).unwrap(),
Some("npm:package@1.0".to_string()),
);
assert_eq!(
resolve("package/some_path.ts", &deps).unwrap(),
Some("npm:/package@1.0/some_path.ts".to_string()),
);

assert_eq!(
resolve("@deno/test", &deps).unwrap(),
Some("npm:@deno/test@~0.2".to_string()),
);
assert_eq!(
resolve("@deno/test/some_path.ts", &deps).unwrap(),
Some("npm:/@deno/test@~0.2/some_path.ts".to_string()),
);
// matches the start, but doesn't have the same length or a path
assert_eq!(resolve("@deno/testing", &deps).unwrap(), None,);

// alias
assert_eq!(
resolve("package-alias", &deps).unwrap(),
Some("npm:package@^1.2".to_string()),
);

// non-existent bare specifier
assert_eq!(resolve("non-existent", &deps).unwrap(), None);
}
}
1 change: 1 addition & 0 deletions cli/tests/testdata/npm/node_modules_import/main.out
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
2
2
2
7 changes: 5 additions & 2 deletions cli/tests/testdata/npm/node_modules_import/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as myImport1 from "@denotest/esm-basic";
import * as myImport2 from "./node_modules/@denotest/esm-basic/main.mjs";
import * as myImport3 from "@denotest/esm-basic/main.mjs";

myImport1.setValue(5);
myImport2.setValue(2);

// these should both give type errors
// these should all give type errors
const value1: string = myImport1.getValue();
const value2: string = myImport2.getValue();
const value3: string = myImport3.getValue();

// these should both be equal because it should be mutating the same module
// these should all be equal because it should be mutating the same module
console.log(value1);
console.log(value2);
console.log(value3);
11 changes: 8 additions & 3 deletions cli/tests/testdata/npm/node_modules_import/main_check.out
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
error: TS2322 [ERROR]: Type 'number' is not assignable to type 'string'.
const value1: string = myImport1.getValue();
~~~~~~
at file:///[WILDCARD]/npm/node_modules_import/main.ts:8:7
at file:///[WILDCARD]/npm/node_modules_import/main.ts:9:7

TS2322 [ERROR]: Type 'number' is not assignable to type 'string'.
const value2: string = myImport2.getValue();
~~~~~~
at file:///[WILDCARD]/npm/node_modules_import/main.ts:9:7
at file:///[WILDCARD]/npm/node_modules_import/main.ts:10:7

TS2322 [ERROR]: Type 'number' is not assignable to type 'string'.
const value3: string = myImport3.getValue();
~~~~~~
at file:///[WILDCARD]/npm/node_modules_import/main.ts:11:7

Found 2 errors.
Found 3 errors.