mirror of
https://github.com/denoland/deno.git
synced 2025-02-02 20:55:35 -05:00
147411e64b
Adds much better support for the unstable Deno workspaces as well as support for npm workspaces. npm workspaces is still lacking in that we only install packages into the root node_modules folder. We'll make it smarter over time in order for it to figure out when to add node_modules folders within packages. This includes a breaking change in config file resolution where we stop searching for config files on the first found package.json unless it's in a workspace. For the previous behaviour, the root deno.json needs to be updated to be a workspace by adding `"workspace": ["./path-to-pkg-json-folder-goes-here"]`. See details in https://github.com/denoland/deno_config/pull/66 Closes #24340 Closes #24159 Closes #24161 Closes #22020 Closes #18546 Closes #16106 Closes #24160
486 lines
14 KiB
Rust
486 lines
14 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use deno_ast::ParsedSource;
|
|
use deno_ast::SourceRange;
|
|
use deno_ast::SourceTextInfo;
|
|
use deno_config::package_json::PackageJsonDepValue;
|
|
use deno_config::workspace::MappedResolution;
|
|
use deno_config::workspace::WorkspaceResolver;
|
|
use deno_core::ModuleSpecifier;
|
|
use deno_graph::DependencyDescriptor;
|
|
use deno_graph::DynamicTemplatePart;
|
|
use deno_graph::ParserModuleAnalyzer;
|
|
use deno_graph::TypeScriptReference;
|
|
use deno_runtime::deno_node::is_builtin_node_module;
|
|
|
|
use crate::resolver::SloppyImportsResolver;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum SpecifierUnfurlerDiagnostic {
|
|
UnanalyzableDynamicImport {
|
|
specifier: ModuleSpecifier,
|
|
text_info: SourceTextInfo,
|
|
range: SourceRange,
|
|
},
|
|
}
|
|
|
|
impl SpecifierUnfurlerDiagnostic {
|
|
pub fn code(&self) -> &'static str {
|
|
match self {
|
|
Self::UnanalyzableDynamicImport { .. } => "unanalyzable-dynamic-import",
|
|
}
|
|
}
|
|
|
|
pub fn message(&self) -> &'static str {
|
|
match self {
|
|
Self::UnanalyzableDynamicImport { .. } => {
|
|
"unable to analyze dynamic import"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct SpecifierUnfurler<'a> {
|
|
sloppy_imports_resolver: Option<&'a SloppyImportsResolver>,
|
|
workspace_resolver: &'a WorkspaceResolver,
|
|
bare_node_builtins: bool,
|
|
}
|
|
|
|
impl<'a> SpecifierUnfurler<'a> {
|
|
pub fn new(
|
|
sloppy_imports_resolver: Option<&'a SloppyImportsResolver>,
|
|
workspace_resolver: &'a WorkspaceResolver,
|
|
bare_node_builtins: bool,
|
|
) -> Self {
|
|
Self {
|
|
sloppy_imports_resolver,
|
|
workspace_resolver,
|
|
bare_node_builtins,
|
|
}
|
|
}
|
|
|
|
fn unfurl_specifier(
|
|
&self,
|
|
referrer: &ModuleSpecifier,
|
|
specifier: &str,
|
|
) -> Option<String> {
|
|
let resolved = if let Ok(resolved) =
|
|
self.workspace_resolver.resolve(specifier, referrer)
|
|
{
|
|
match resolved {
|
|
MappedResolution::Normal(specifier)
|
|
| MappedResolution::ImportMap(specifier) => Some(specifier),
|
|
MappedResolution::PackageJson {
|
|
sub_path,
|
|
dep_result,
|
|
..
|
|
} => match dep_result {
|
|
Ok(dep) => match dep {
|
|
PackageJsonDepValue::Req(req) => ModuleSpecifier::parse(&format!(
|
|
"npm:{}{}",
|
|
req,
|
|
sub_path
|
|
.as_ref()
|
|
.map(|s| format!("/{}", s))
|
|
.unwrap_or_default()
|
|
))
|
|
.ok(),
|
|
PackageJsonDepValue::Workspace(_) => {
|
|
log::warn!(
|
|
"package.json workspace entries are not implemented yet for publishing."
|
|
);
|
|
None
|
|
}
|
|
},
|
|
Err(err) => {
|
|
log::warn!(
|
|
"Ignoring failed to resolve package.json dependency. {:#}",
|
|
err
|
|
);
|
|
None
|
|
}
|
|
},
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let resolved = match resolved {
|
|
Some(resolved) => resolved,
|
|
None if self.bare_node_builtins && is_builtin_node_module(specifier) => {
|
|
format!("node:{specifier}").parse().unwrap()
|
|
}
|
|
None => ModuleSpecifier::options()
|
|
.base_url(Some(referrer))
|
|
.parse(specifier)
|
|
.ok()?,
|
|
};
|
|
// TODO(lucacasonato): this requires integration in deno_graph first
|
|
// let resolved = if let Ok(specifier) =
|
|
// NpmPackageReqReference::from_specifier(&resolved)
|
|
// {
|
|
// if let Some(scope_name) = specifier.req().name.strip_prefix("@jsr/") {
|
|
// let (scope, name) = scope_name.split_once("__")?;
|
|
// let new_specifier = JsrPackageReqReference::new(PackageReqReference {
|
|
// req: PackageReq {
|
|
// name: format!("@{scope}/{name}"),
|
|
// version_req: specifier.req().version_req.clone(),
|
|
// },
|
|
// sub_path: specifier.sub_path().map(ToOwned::to_owned),
|
|
// })
|
|
// .to_string();
|
|
// ModuleSpecifier::parse(&new_specifier).unwrap()
|
|
// } else {
|
|
// resolved
|
|
// }
|
|
// } else {
|
|
// resolved
|
|
// };
|
|
let resolved =
|
|
if let Some(sloppy_imports_resolver) = self.sloppy_imports_resolver {
|
|
sloppy_imports_resolver
|
|
.resolve(&resolved, deno_graph::source::ResolutionMode::Execution)
|
|
.as_specifier()
|
|
.clone()
|
|
} else {
|
|
resolved
|
|
};
|
|
let relative_resolved = relative_url(&resolved, referrer);
|
|
if relative_resolved == specifier {
|
|
None // nothing to unfurl
|
|
} else {
|
|
Some(relative_resolved)
|
|
}
|
|
}
|
|
|
|
/// Attempts to unfurl the dynamic dependency returning `true` on success
|
|
/// or `false` when the import was not analyzable.
|
|
fn try_unfurl_dynamic_dep(
|
|
&self,
|
|
module_url: &lsp_types::Url,
|
|
text_info: &SourceTextInfo,
|
|
dep: &deno_graph::DynamicDependencyDescriptor,
|
|
text_changes: &mut Vec<deno_ast::TextChange>,
|
|
) -> bool {
|
|
match &dep.argument {
|
|
deno_graph::DynamicArgument::String(specifier) => {
|
|
let range = to_range(text_info, &dep.argument_range);
|
|
let maybe_relative_index =
|
|
text_info.text_str()[range.start..range.end].find(specifier);
|
|
let Some(relative_index) = maybe_relative_index else {
|
|
return true; // always say it's analyzable for a string
|
|
};
|
|
let unfurled = self.unfurl_specifier(module_url, specifier);
|
|
if let Some(unfurled) = unfurled {
|
|
let start = range.start + relative_index;
|
|
text_changes.push(deno_ast::TextChange {
|
|
range: start..start + specifier.len(),
|
|
new_text: unfurled,
|
|
});
|
|
}
|
|
true
|
|
}
|
|
deno_graph::DynamicArgument::Template(parts) => match parts.first() {
|
|
Some(DynamicTemplatePart::String { value: specifier }) => {
|
|
// relative doesn't need to be modified
|
|
let is_relative =
|
|
specifier.starts_with("./") || specifier.starts_with("../");
|
|
if is_relative {
|
|
return true;
|
|
}
|
|
if !specifier.ends_with('/') {
|
|
return false;
|
|
}
|
|
let unfurled = self.unfurl_specifier(module_url, specifier);
|
|
let Some(unfurled) = unfurled else {
|
|
return true; // nothing to unfurl
|
|
};
|
|
let range = to_range(text_info, &dep.argument_range);
|
|
let maybe_relative_index =
|
|
text_info.text_str()[range.start..].find(specifier);
|
|
let Some(relative_index) = maybe_relative_index else {
|
|
return false;
|
|
};
|
|
let start = range.start + relative_index;
|
|
text_changes.push(deno_ast::TextChange {
|
|
range: start..start + specifier.len(),
|
|
new_text: unfurled,
|
|
});
|
|
true
|
|
}
|
|
Some(DynamicTemplatePart::Expr) => {
|
|
false // failed analyzing
|
|
}
|
|
None => {
|
|
true // ignore
|
|
}
|
|
},
|
|
deno_graph::DynamicArgument::Expr => {
|
|
false // failed analyzing
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn unfurl(
|
|
&self,
|
|
url: &ModuleSpecifier,
|
|
parsed_source: &ParsedSource,
|
|
diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic),
|
|
) -> String {
|
|
let mut text_changes = Vec::new();
|
|
let text_info = parsed_source.text_info_lazy();
|
|
let module_info = ParserModuleAnalyzer::module_info(parsed_source);
|
|
let analyze_specifier =
|
|
|specifier: &str,
|
|
range: &deno_graph::PositionRange,
|
|
text_changes: &mut Vec<deno_ast::TextChange>| {
|
|
if let Some(unfurled) = self.unfurl_specifier(url, specifier) {
|
|
text_changes.push(deno_ast::TextChange {
|
|
range: to_range(text_info, range),
|
|
new_text: unfurled,
|
|
});
|
|
}
|
|
};
|
|
for dep in &module_info.dependencies {
|
|
match dep {
|
|
DependencyDescriptor::Static(dep) => {
|
|
analyze_specifier(
|
|
&dep.specifier,
|
|
&dep.specifier_range,
|
|
&mut text_changes,
|
|
);
|
|
}
|
|
DependencyDescriptor::Dynamic(dep) => {
|
|
let success =
|
|
self.try_unfurl_dynamic_dep(url, text_info, dep, &mut text_changes);
|
|
|
|
if !success {
|
|
let start_pos = text_info.line_start(dep.argument_range.start.line)
|
|
+ dep.argument_range.start.character;
|
|
let end_pos = text_info.line_start(dep.argument_range.end.line)
|
|
+ dep.argument_range.end.character;
|
|
diagnostic_reporter(
|
|
SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport {
|
|
specifier: url.to_owned(),
|
|
range: SourceRange::new(start_pos, end_pos),
|
|
text_info: text_info.clone(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for ts_ref in &module_info.ts_references {
|
|
let specifier_with_range = match ts_ref {
|
|
TypeScriptReference::Path(range) => range,
|
|
TypeScriptReference::Types(range) => range,
|
|
};
|
|
analyze_specifier(
|
|
&specifier_with_range.text,
|
|
&specifier_with_range.range,
|
|
&mut text_changes,
|
|
);
|
|
}
|
|
for specifier_with_range in &module_info.jsdoc_imports {
|
|
analyze_specifier(
|
|
&specifier_with_range.text,
|
|
&specifier_with_range.range,
|
|
&mut text_changes,
|
|
);
|
|
}
|
|
if let Some(specifier_with_range) = &module_info.jsx_import_source {
|
|
analyze_specifier(
|
|
&specifier_with_range.text,
|
|
&specifier_with_range.range,
|
|
&mut text_changes,
|
|
);
|
|
}
|
|
|
|
let rewritten_text =
|
|
deno_ast::apply_text_changes(text_info.text_str(), text_changes);
|
|
rewritten_text
|
|
}
|
|
}
|
|
|
|
fn relative_url(
|
|
resolved: &ModuleSpecifier,
|
|
referrer: &ModuleSpecifier,
|
|
) -> String {
|
|
if resolved.scheme() == "file" {
|
|
let relative = referrer.make_relative(resolved).unwrap();
|
|
if relative.is_empty() {
|
|
let last = resolved.path_segments().unwrap().last().unwrap();
|
|
format!("./{last}")
|
|
} else if relative.starts_with("../") {
|
|
relative
|
|
} else {
|
|
format!("./{relative}")
|
|
}
|
|
} else {
|
|
resolved.to_string()
|
|
}
|
|
}
|
|
|
|
fn to_range(
|
|
text_info: &SourceTextInfo,
|
|
range: &deno_graph::PositionRange,
|
|
) -> std::ops::Range<usize> {
|
|
let mut range = range
|
|
.as_source_range(text_info)
|
|
.as_byte_range(text_info.range().start);
|
|
let text = &text_info.text_str()[range.clone()];
|
|
if text.starts_with('"') || text.starts_with('\'') {
|
|
range.start += 1;
|
|
}
|
|
if text.ends_with('"') || text.ends_with('\'') {
|
|
range.end -= 1;
|
|
}
|
|
range
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::Arc;
|
|
|
|
use super::*;
|
|
use deno_ast::MediaType;
|
|
use deno_ast::ModuleSpecifier;
|
|
use deno_core::serde_json::json;
|
|
use deno_core::url::Url;
|
|
use deno_runtime::deno_fs::RealFs;
|
|
use deno_runtime::deno_node::PackageJson;
|
|
use import_map::ImportMapWithDiagnostics;
|
|
use pretty_assertions::assert_eq;
|
|
use test_util::testdata_path;
|
|
|
|
fn parse_ast(specifier: &Url, source_code: &str) -> ParsedSource {
|
|
let media_type = MediaType::from_specifier(specifier);
|
|
deno_ast::parse_module(deno_ast::ParseParams {
|
|
specifier: specifier.clone(),
|
|
media_type,
|
|
capture_tokens: false,
|
|
maybe_syntax: None,
|
|
scope_analysis: false,
|
|
text: source_code.into(),
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_unfurling() {
|
|
let cwd = testdata_path().join("unfurl").to_path_buf();
|
|
|
|
let deno_json_url =
|
|
ModuleSpecifier::from_file_path(cwd.join("deno.json")).unwrap();
|
|
let value = json!({
|
|
"imports": {
|
|
"express": "npm:express@5",
|
|
"lib/": "./lib/",
|
|
"fizz": "./fizz/mod.ts",
|
|
"@std/fs": "npm:@jsr/std__fs@1",
|
|
}
|
|
});
|
|
let ImportMapWithDiagnostics { import_map, .. } =
|
|
import_map::parse_from_value(deno_json_url, value).unwrap();
|
|
let package_json = PackageJson::load_from_value(
|
|
cwd.join("package.json"),
|
|
json!({
|
|
"dependencies": {
|
|
"chalk": 5
|
|
}
|
|
}),
|
|
);
|
|
let workspace_resolver = WorkspaceResolver::new_raw(
|
|
Some(import_map),
|
|
vec![Arc::new(package_json)],
|
|
deno_config::workspace::PackageJsonDepResolution::Enabled,
|
|
);
|
|
let fs = Arc::new(RealFs);
|
|
let sloppy_imports_resolver = SloppyImportsResolver::new(fs);
|
|
|
|
let unfurler = SpecifierUnfurler::new(
|
|
Some(&sloppy_imports_resolver),
|
|
&workspace_resolver,
|
|
true,
|
|
);
|
|
|
|
// Unfurling TS file should apply changes.
|
|
{
|
|
let source_code = r#"import express from "express";"
|
|
import foo from "lib/foo.ts";
|
|
import bar from "lib/bar.ts";
|
|
import fizz from "fizz";
|
|
import chalk from "chalk";
|
|
import baz from "./baz";
|
|
import b from "./b.js";
|
|
import b2 from "./b";
|
|
import "./mod.ts";
|
|
import url from "url";
|
|
// TODO: unfurl these to jsr
|
|
// import "npm:@jsr/std__fs@1/file";
|
|
// import "npm:@jsr/std__fs@1";
|
|
// import "npm:@jsr/std__fs";
|
|
// import "@std/fs";
|
|
|
|
const test1 = await import("lib/foo.ts");
|
|
const test2 = await import(`lib/foo.ts`);
|
|
const test3 = await import(`lib/${expr}`);
|
|
const test4 = await import(`./lib/${expr}`);
|
|
const test5 = await import("./lib/something.ts");
|
|
const test6 = await import(`./lib/something.ts`);
|
|
// will warn
|
|
const warn1 = await import(`lib${expr}`);
|
|
const warn2 = await import(`${expr}`);
|
|
"#;
|
|
let specifier =
|
|
ModuleSpecifier::from_file_path(cwd.join("mod.ts")).unwrap();
|
|
let source = parse_ast(&specifier, source_code);
|
|
let mut d = Vec::new();
|
|
let mut reporter = |diagnostic| d.push(diagnostic);
|
|
let unfurled_source = unfurler.unfurl(&specifier, &source, &mut reporter);
|
|
assert_eq!(d.len(), 2);
|
|
assert!(
|
|
matches!(
|
|
d[0],
|
|
SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. }
|
|
),
|
|
"{:?}",
|
|
d[0]
|
|
);
|
|
assert!(
|
|
matches!(
|
|
d[1],
|
|
SpecifierUnfurlerDiagnostic::UnanalyzableDynamicImport { .. }
|
|
),
|
|
"{:?}",
|
|
d[1]
|
|
);
|
|
let expected_source = r#"import express from "npm:express@5";"
|
|
import foo from "./lib/foo.ts";
|
|
import bar from "./lib/bar.ts";
|
|
import fizz from "./fizz/mod.ts";
|
|
import chalk from "npm:chalk@5";
|
|
import baz from "./baz/index.js";
|
|
import b from "./b.ts";
|
|
import b2 from "./b.ts";
|
|
import "./mod.ts";
|
|
import url from "node:url";
|
|
// TODO: unfurl these to jsr
|
|
// import "npm:@jsr/std__fs@1/file";
|
|
// import "npm:@jsr/std__fs@1";
|
|
// import "npm:@jsr/std__fs";
|
|
// import "@std/fs";
|
|
|
|
const test1 = await import("./lib/foo.ts");
|
|
const test2 = await import(`./lib/foo.ts`);
|
|
const test3 = await import(`./lib/${expr}`);
|
|
const test4 = await import(`./lib/${expr}`);
|
|
const test5 = await import("./lib/something.ts");
|
|
const test6 = await import(`./lib/something.ts`);
|
|
// will warn
|
|
const warn1 = await import(`lib${expr}`);
|
|
const warn2 = await import(`${expr}`);
|
|
"#;
|
|
assert_eq!(unfurled_source, expected_source);
|
|
}
|
|
}
|
|
}
|