Skip to content

feat: support workspaces and use deno_resolver #462

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

Merged
merged 11 commits into from
May 8, 2025
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
1,186 changes: 834 additions & 352 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,14 @@ members = [
]

[workspace.dependencies]
deno_error = "0.5.6"
async-trait = "0.1.88"
deno_config = "0.54.2"
deno_error = { version = "0.5.6", features = ["serde", "serde_json", "url"] }
deno_path_util = "0.3.2"
deno_resolver = { version = "0.35.0", features = ["graph"] }
serde_json = { version = "1.0.140", features = ["preserve_order"] }
sys_traits = { version = "0.1.9", features = ["real"] }
url = { version = "2.5.4", features =["serde"] }

[patch.crates-io]
deno_resolver = { git = "https://github.com/denoland/deno", rev = "59ffc1987c67f570e2081457c898efe51c872945" }
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,22 +606,18 @@ await build({
});
```

### Import Map / deno.json Support
### deno.json Support

To use an import map or deno.json file with `"imports"` and/or `"scopes"`, add
an `importMap` entry to your build object:
Starting in dnt 0.42, the deno.json is auto-discovered. A config file can be
explicitly specified by the `configFile` key:

```ts
await build({
// ...etc...
importMap: "deno.json",
configFile: import.meta.resolve("../deno.json"),
});
```

Note there is no support for the deno.json `importMap` key. Either embed that in
your deno.json or specify the import map in this property directly. Also note
that the deno.json is not auto-discovered—you must explicitly specify it.

### GitHub Actions - Npm Publish on Tag

1. Ensure your build script accepts a version as a CLI argument and sets that in
Expand Down
5 changes: 5 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export interface BuildOptions {
mappings?: SpecifierMappings;
/** Package.json output. You may override dependencies and dev dependencies in here. */
package: PackageJson;
/** Path or url to a deno.json. */
configFile?: string;
/** Path or url to import map. */
importMap?: string;
/** Package manager used to install dependencies and run npm scripts.
Expand Down Expand Up @@ -209,6 +211,7 @@ export async function build(options: BuildOptions): Promise<void> {
? "inline"
: options.declaration ?? "inline",
};
const cwd = Deno.cwd();
const declarationMap = options.declarationMap ??
(!!options.declaration && !options.skipSourceOutput);
const packageManager = options.packageManager ?? "npm";
Expand Down Expand Up @@ -586,7 +589,9 @@ export async function build(options: BuildOptions): Promise<void> {
mappings: options.mappings,
target: scriptTarget,
importMap: options.importMap,
configFile: options.configFile,
internalWasmUrl: options.internalWasmUrl,
cwd: path.toFileUrl(cwd).toString(),
});
}

Expand Down
9 changes: 7 additions & 2 deletions rs-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ serialization = ["serde"]

[dependencies]
anyhow = "1.0.70"
async-trait.workspace = true
base64 = "0.13.1"
deno_ast = { version = "0.46.6", features = ["transforms", "view", "visit", "utils"] }
deno_config.workspace = true
deno_error.workspace = true
deno_graph = { version = "0.90.0", features = [], default-features = false }
deno_path_util = "0.3.2"
deno_path_util.workspace = true
deno_resolver.workspace = true
deno_semver = "0.7.1"
futures = "0.3.25"
import_map = { version = "0.21.0", features = ["ext"] }
Expand All @@ -28,9 +31,11 @@ pathdiff = "0.2.1"
regex = "1.7"
reqwest = { version = "0.11", features = ["rustls"], optional = true }
serde = { version = "1.0.159", features = ["derive"], optional = true }
serde_json = "1.0.96"
serde_json.workspace = true
sys_traits.workspace = true
tokio = { version = "1", features = ["full"], optional = true }
url.workspace = true

[dev-dependencies]
pretty_assertions = "1.3.0"
sys_traits = { workspace = true, features = ["memory"] }
136 changes: 42 additions & 94 deletions rs-lib/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,33 @@ use crate::specifiers::get_specifiers;
use crate::specifiers::Specifiers;
use crate::MappedSpecifier;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use deno_ast::ModuleSpecifier;
use deno_ast::ParseDiagnostic;
use deno_ast::ParsedSource;
use deno_error::JsErrorBox;
use deno_graph::source::CacheSetting;
use deno_graph::source::ResolutionKind;
use deno_graph::source::ResolveError;
use deno_config::workspace::WorkspaceDirectory;
use deno_graph::CapturingModuleAnalyzer;
use deno_graph::EsParser;
use deno_graph::JsModule;
use deno_graph::Module;
use deno_graph::ParseOptions;
use deno_graph::ParsedSourceStore;
use deno_graph::Range;
use import_map::ImportMapOptions;
use deno_resolver::factory::WorkspaceFactorySys;
use deno_resolver::graph::DefaultDenoResolverRc;
use deno_resolver::npm::DenoInNpmPackageChecker;
use deno_resolver::workspace::ScopedJsxImportSourceConfig;
use sys_traits::impls::RealSys;

pub struct ModuleGraphOptions<'a> {
pub struct ModuleGraphOptions<'a, TSys: WorkspaceFactorySys> {
pub entry_points: Vec<ModuleSpecifier>,
pub test_entry_points: Vec<ModuleSpecifier>,
pub loader: Option<Rc<dyn Loader>>,
pub loader: Rc<dyn Loader>,
pub resolver: DefaultDenoResolverRc<TSys>,
pub specifier_mappings: &'a HashMap<ModuleSpecifier, MappedSpecifier>,
pub import_map: Option<ModuleSpecifier>,
pub cjs_tracker:
Rc<deno_resolver::cjs::CjsTracker<DenoInNpmPackageChecker, TSys>>,
pub workspace_dir: Rc<WorkspaceDirectory>,
}

/// Wrapper around deno_graph::ModuleGraph.
Expand All @@ -45,32 +49,26 @@ pub struct ModuleGraph {
}

impl ModuleGraph {
pub async fn build_with_specifiers(
options: ModuleGraphOptions<'_>,
pub async fn build_with_specifiers<TSys: WorkspaceFactorySys>(
options: ModuleGraphOptions<'_, TSys>,
) -> Result<(Self, Specifiers)> {
let loader = options.loader.unwrap_or_else(|| {
#[cfg(feature = "tokio-loader")]
return Rc::new(crate::loader::DefaultLoader::new());
#[cfg(not(feature = "tokio-loader"))]
panic!("You must provide a loader or use the 'tokio-loader' feature.")
});
let resolver = match options.import_map {
Some(import_map_url) => Some(
ImportMapResolver::load(&import_map_url, &*loader)
.await
.context("Error loading import map.")?,
),
None => None,
};
let resolver = options.resolver;
let loader = options.loader;
let loader = SourceLoader::new(
loader,
get_all_specifier_mappers(),
options.specifier_mappings,
);
let scoped_jsx_import_source_config =
ScopedJsxImportSourceConfig::from_workspace_dir(&options.workspace_dir)?;
let source_parser = ScopeAnalysisParser;
let capturing_analyzer =
CapturingModuleAnalyzer::new(Some(Box::new(source_parser)), None);
let mut graph = deno_graph::ModuleGraph::new(deno_graph::GraphKind::All);
let graph_resolver = resolver.as_graph_resolver(
&options.cjs_tracker,
&scoped_jsx_import_source_config,
);
graph
.build(
options
Expand All @@ -84,7 +82,7 @@ impl ModuleGraph {
is_dynamic: false,
skip_dynamic_deps: false,
imports: Default::default(),
resolver: resolver.as_ref().map(|r| r.as_resolver()),
resolver: Some(&graph_resolver),
locker: None,
module_analyzer: &capturing_analyzer,
reporter: None,
Expand Down Expand Up @@ -181,17 +179,22 @@ impl ModuleGraph {
})
}

pub fn get_parsed_source(&self, specifier: &ModuleSpecifier) -> ParsedSource {
let specifier = self.graph.resolve(specifier);
self
pub fn get_parsed_source(
&self,
js_module: &JsModule,
) -> Result<ParsedSource, ParseDiagnostic> {
match self
.capturing_analyzer
.get_parsed_source(&specifier)
.unwrap_or_else(|| {
panic!(
"dnt bug - Did not find parsed source for specifier: {}",
specifier
);
})
.get_parsed_source(&js_module.specifier)
{
Some(parsed_source) => Ok(parsed_source),
None => self.capturing_analyzer.parse_program(ParseOptions {
specifier: &js_module.specifier,
source: js_module.source.clone(),
media_type: js_module.media_type,
scope_analysis: false,
}),
}
}

pub fn resolve_dependency(
Expand All @@ -202,7 +205,7 @@ impl ModuleGraph {
self
.graph
.resolve_dependency(value, referrer, /* prefer_types */ false)
.map(|url| url.clone())
.cloned()
.or_else(|| {
let value_lower = value.to_lowercase();
if value_lower.starts_with("https://")
Expand Down Expand Up @@ -236,58 +239,3 @@ fn format_specifiers_for_message(
.collect::<Vec<_>>()
.join("\n")
}

#[derive(Debug)]
struct ImportMapResolver(import_map::ImportMap);

impl ImportMapResolver {
pub async fn load(
import_map_url: &ModuleSpecifier,
loader: &dyn Loader,
) -> Result<Self> {
let response = loader
.load(import_map_url.clone(), CacheSetting::Use, None)
.await?
.ok_or_else(|| anyhow!("Could not find {}", import_map_url))?;
let value = jsonc_parser::parse_to_serde_value(
&String::from_utf8(response.content)?,
&jsonc_parser::ParseOptions {
allow_comments: true,
allow_loose_object_property_names: true,
allow_trailing_commas: true,
},
)?
.unwrap_or_else(|| serde_json::Value::Object(Default::default()));
let result = import_map::parse_from_value_with_options(
import_map_url.clone(),
value,
ImportMapOptions {
address_hook: None,
expand_imports: true,
},
)?;
// if !result.diagnostics.is_empty() {
// todo: surface diagnostics maybe? It seems like this should not be hard error according to import map spec
// bail!("Import map diagnostics:\n{}", result.diagnostics.into_iter().map(|d| format!(" - {}", d)).collect::<Vec<_>>().join("\n"));
//}
Ok(ImportMapResolver(result.import_map))
}

pub fn as_resolver(&self) -> &dyn deno_graph::source::Resolver {
self
}
}

impl deno_graph::source::Resolver for ImportMapResolver {
fn resolve(
&self,
specifier: &str,
referrer_range: &Range,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, ResolveError> {
self
.0
.resolve(specifier, &referrer_range.specifier)
.map_err(|err| ResolveError::Other(JsErrorBox::from_err(err)))
}
}
Loading