summaryrefslogtreecommitdiff
path: root/cli/module_graph2.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/module_graph2.rs')
-rw-r--r--cli/module_graph2.rs500
1 files changed, 385 insertions, 115 deletions
diff --git a/cli/module_graph2.rs b/cli/module_graph2.rs
index 3fc900373..6ac27906d 100644
--- a/cli/module_graph2.rs
+++ b/cli/module_graph2.rs
@@ -1,9 +1,9 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+use crate::ast;
use crate::ast::parse;
use crate::ast::transpile_module;
use crate::ast::BundleHook;
-use crate::ast::EmitOptions;
use crate::ast::Location;
use crate::ast::ParsedModule;
use crate::colors;
@@ -22,8 +22,7 @@ use crate::specifier_handler::DependencyMap;
use crate::specifier_handler::Emit;
use crate::specifier_handler::FetchFuture;
use crate::specifier_handler::SpecifierHandler;
-use crate::tsc2::exec;
-use crate::tsc2::Request;
+use crate::tsc2;
use crate::tsc_config::IgnoredCompilerOptions;
use crate::tsc_config::TsConfig;
use crate::version;
@@ -35,6 +34,7 @@ use deno_core::futures::stream::StreamExt;
use deno_core::serde::Serialize;
use deno_core::serde::Serializer;
use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
use deno_core::ModuleResolutionError;
use deno_core::ModuleSpecifier;
use regex::Regex;
@@ -121,13 +121,13 @@ impl Error for GraphError {}
struct BundleLoader<'a> {
cm: Rc<swc_common::SourceMap>,
graph: &'a Graph2,
- emit_options: &'a EmitOptions,
+ emit_options: &'a ast::EmitOptions,
}
impl<'a> BundleLoader<'a> {
pub fn new(
graph: &'a Graph2,
- emit_options: &'a EmitOptions,
+ emit_options: &'a ast::EmitOptions,
cm: Rc<swc_common::SourceMap>,
) -> Self {
BundleLoader {
@@ -480,7 +480,7 @@ impl Module {
}
}
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Stats(pub Vec<(String, u128)>);
impl<'de> Deserialize<'de> for Stats {
@@ -504,6 +504,23 @@ impl fmt::Display for Stats {
}
}
+/// A structure that provides information about a module graph result.
+#[derive(Debug, Default)]
+pub struct ResultInfo {
+ /// A structure which provides diagnostic information (usually from `tsc`)
+ /// about the code in the module graph.
+ pub diagnostics: Diagnostics,
+ /// Optionally ignored compiler options that represent any options that were
+ /// ignored if there was a user provided configuration.
+ pub maybe_ignored_options: Option<IgnoredCompilerOptions>,
+ /// A structure providing key metrics around the operation performed, in
+ /// milliseconds.
+ pub stats: Stats,
+}
+
+/// Represents the "default" type library that should be used when type
+/// checking the code in the module graph. Note that a user provided config
+/// of `"lib"` would override this value.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TypeLib {
DenoWindow,
@@ -539,7 +556,11 @@ impl Serialize for TypeLib {
#[derive(Debug, Default)]
pub struct BundleOptions {
+ /// If `true` then debug logging will be output from the isolate.
pub debug: bool,
+ /// An optional string that points to a user supplied TypeScript configuration
+ /// file that augments the the default configuration passed to the TypeScript
+ /// compiler.
pub maybe_config_path: Option<String>,
}
@@ -560,6 +581,35 @@ pub struct CheckOptions {
pub reload: bool,
}
+#[derive(Debug, Eq, PartialEq)]
+pub enum BundleType {
+ /// Return the emitted contents of the program as a single "flattened" ES
+ /// module.
+ Esm,
+ // TODO(@kitsonk) once available in swc
+ // Iife,
+ /// Do not bundle the emit, instead returning each of the modules that are
+ /// part of the program as individual files.
+ None,
+}
+
+impl Default for BundleType {
+ fn default() -> Self {
+ BundleType::None
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct EmitOptions {
+ /// Indicate the form the result of the emit should take.
+ pub bundle_type: BundleType,
+ /// If `true` then debug logging will be output from the isolate.
+ pub debug: bool,
+ /// An optional map that contains user supplied TypeScript compiler
+ /// configuration options that are passed to the TypeScript compiler.
+ pub maybe_user_config: Option<HashMap<String, Value>>,
+}
+
/// A structure which provides options when transpiling modules.
#[derive(Debug, Default)]
pub struct TranspileOptions {
@@ -647,47 +697,8 @@ impl Graph2 {
}));
let maybe_ignored_options =
ts_config.merge_tsconfig(options.maybe_config_path)?;
- let emit_options: EmitOptions = ts_config.into();
- let cm = Rc::new(swc_common::SourceMap::new(
- swc_common::FilePathMapping::empty(),
- ));
- let loader = BundleLoader::new(self, &emit_options, cm.clone());
- let hook = Box::new(BundleHook);
- let globals = swc_common::Globals::new();
- let bundler = swc_bundler::Bundler::new(
- &globals,
- cm.clone(),
- loader,
- self,
- swc_bundler::Config::default(),
- hook,
- );
- let mut entries = HashMap::new();
- entries.insert(
- "bundle".to_string(),
- swc_common::FileName::Custom(root_specifier.to_string()),
- );
- let output = bundler
- .bundle(entries)
- .context("Unable to output bundle during Graph2::bundle().")?;
- let mut buf = Vec::new();
- {
- let mut emitter = swc_ecmascript::codegen::Emitter {
- cfg: swc_ecmascript::codegen::Config { minify: false },
- cm: cm.clone(),
- comments: None,
- wr: Box::new(swc_ecmascript::codegen::text_writer::JsWriter::new(
- cm, "\n", &mut buf, None,
- )),
- };
-
- emitter
- .emit_module(&output[0].module)
- .context("Unable to emit bundle during Graph2::bundle().")?;
- }
- let s = String::from_utf8(buf)
- .context("Emitted bundle is an invalid utf-8 string.")?;
+ let s = self.emit_bundle(&root_specifier, &ts_config.into())?;
let stats = Stats(vec![
("Files".to_string(), self.modules.len() as u128),
("Total time".to_string(), start.elapsed().as_millis()),
@@ -697,11 +708,7 @@ impl Graph2 {
}
/// Type check the module graph, corresponding to the options provided.
- pub fn check(
- self,
- options: CheckOptions,
- ) -> Result<(Stats, Diagnostics, Option<IgnoredCompilerOptions>), AnyError>
- {
+ pub fn check(self, options: CheckOptions) -> Result<ResultInfo, AnyError> {
let mut config = TsConfig::new(json!({
"allowJs": true,
// TODO(@kitsonk) is this really needed?
@@ -745,11 +752,10 @@ impl Graph2 {
&& (!options.reload || self.roots_dynamic))
{
debug!("graph does not need to be checked or emitted.");
- return Ok((
- Stats(Vec::new()),
- Diagnostics::default(),
+ return Ok(ResultInfo {
maybe_ignored_options,
- ));
+ ..Default::default()
+ });
}
// TODO(@kitsonk) not totally happy with this here, but this is the first
@@ -760,26 +766,15 @@ impl Graph2 {
info!("{} {}", colors::green("Check"), specifier);
}
- let root_names: Vec<(ModuleSpecifier, MediaType)> = self
- .roots
- .iter()
- .map(|ms| {
- (
- // root modules can be redirects, so before we pass it to tsc we need
- // to resolve the redirect
- self.resolve_specifier(ms).clone(),
- self.get_media_type(ms).unwrap(),
- )
- })
- .collect();
+ let root_names = self.get_root_names();
let maybe_tsbuildinfo = self.maybe_tsbuildinfo.clone();
let hash_data =
vec![config.as_bytes(), version::DENO.as_bytes().to_owned()];
let graph = Rc::new(RefCell::new(self));
- let response = exec(
+ let response = tsc2::exec(
js::compiler_isolate_init(),
- Request {
+ tsc2::Request {
config: config.clone(),
debug: options.debug,
graph: graph.clone(),
@@ -837,7 +832,11 @@ impl Graph2 {
}
graph.flush()?;
- Ok((response.stats, response.diagnostics, maybe_ignored_options))
+ Ok(ResultInfo {
+ diagnostics: response.diagnostics,
+ maybe_ignored_options,
+ stats: response.stats,
+ })
}
fn contains_module(&self, specifier: &ModuleSpecifier) -> bool {
@@ -845,6 +844,165 @@ impl Graph2 {
self.modules.contains_key(s)
}
+ /// Emit the module graph in a specific format. This is specifically designed
+ /// to be an "all-in-one" API for access by the runtime, allowing both
+ /// emitting single modules as well as bundles, using Deno module resolution
+ /// or supplied sources.
+ pub fn emit(
+ self,
+ options: EmitOptions,
+ ) -> Result<(HashMap<String, String>, ResultInfo), AnyError> {
+ let mut config = TsConfig::new(json!({
+ "allowJs": true,
+ // TODO(@kitsonk) consider enabling this by default
+ // see: https://github.com/denoland/deno/issues/7732
+ "emitDecoratorMetadata": false,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "isolatedModules": true,
+ "jsx": "react",
+ "lib": TypeLib::DenoWindow,
+ "module": "esnext",
+ "strict": true,
+ "target": "esnext",
+ }));
+ let opts = match options.bundle_type {
+ BundleType::Esm => json!({
+ "checkJs": false,
+ "inlineSourceMap": false,
+ "noEmit": true,
+ "jsxFactory": "React.createElement",
+ "jsxFragmentFactory": "React.Fragment",
+ }),
+ BundleType::None => json!({
+ "outDir": "deno://",
+ "removeComments": true,
+ "sourceMap": true,
+ }),
+ };
+ config.merge(&opts);
+ let maybe_ignored_options =
+ if let Some(user_options) = &options.maybe_user_config {
+ config.merge_user_config(user_options)?
+ } else {
+ None
+ };
+
+ let root_names = self.get_root_names();
+ let hash_data =
+ vec![config.as_bytes(), version::DENO.as_bytes().to_owned()];
+ let graph = Rc::new(RefCell::new(self));
+
+ let response = tsc2::exec(
+ js::compiler_isolate_init(),
+ tsc2::Request {
+ config: config.clone(),
+ debug: options.debug,
+ graph: graph.clone(),
+ hash_data,
+ maybe_tsbuildinfo: None,
+ root_names,
+ },
+ )?;
+
+ let mut emitted_files = HashMap::new();
+ match options.bundle_type {
+ BundleType::Esm => {
+ assert!(
+ response.emitted_files.is_empty(),
+ "No files should have been emitted from tsc."
+ );
+ let graph = graph.borrow();
+ assert_eq!(
+ graph.roots.len(),
+ 1,
+ "Only a single root module supported."
+ );
+ let specifier = &graph.roots[0];
+ let s = graph.emit_bundle(specifier, &config.into())?;
+ emitted_files.insert("deno:///bundle.js".to_string(), s);
+ }
+ BundleType::None => {
+ for emitted_file in &response.emitted_files {
+ assert!(
+ emitted_file.maybe_specifiers.is_some(),
+ "Orphaned file emitted."
+ );
+ let specifiers = emitted_file.maybe_specifiers.clone().unwrap();
+ assert_eq!(
+ specifiers.len(),
+ 1,
+ "An unexpected number of specifiers associated with emitted file."
+ );
+ let specifier = specifiers[0].clone();
+ let extension = match emitted_file.media_type {
+ MediaType::JavaScript => ".js",
+ MediaType::SourceMap => ".js.map",
+ _ => unreachable!(),
+ };
+ let key = format!("{}{}", specifier, extension);
+ emitted_files.insert(key, emitted_file.data.clone());
+ }
+ }
+ };
+
+ Ok((
+ emitted_files,
+ ResultInfo {
+ diagnostics: response.diagnostics,
+ maybe_ignored_options,
+ stats: response.stats,
+ },
+ ))
+ }
+
+ /// Shared between `bundle()` and `emit()`.
+ fn emit_bundle(
+ &self,
+ specifier: &ModuleSpecifier,
+ emit_options: &ast::EmitOptions,
+ ) -> Result<String, AnyError> {
+ let cm = Rc::new(swc_common::SourceMap::new(
+ swc_common::FilePathMapping::empty(),
+ ));
+ let loader = BundleLoader::new(self, emit_options, cm.clone());
+ let hook = Box::new(BundleHook);
+ let globals = swc_common::Globals::new();
+ let bundler = swc_bundler::Bundler::new(
+ &globals,
+ cm.clone(),
+ loader,
+ self,
+ swc_bundler::Config::default(),
+ hook,
+ );
+ let mut entries = HashMap::new();
+ entries.insert(
+ "bundle".to_string(),
+ swc_common::FileName::Custom(specifier.to_string()),
+ );
+ let output = bundler
+ .bundle(entries)
+ .context("Unable to output bundle during Graph2::bundle().")?;
+ let mut buf = Vec::new();
+ {
+ let mut emitter = swc_ecmascript::codegen::Emitter {
+ cfg: swc_ecmascript::codegen::Config { minify: false },
+ cm: cm.clone(),
+ comments: None,
+ wr: Box::new(swc_ecmascript::codegen::text_writer::JsWriter::new(
+ cm, "\n", &mut buf, None,
+ )),
+ };
+
+ emitter
+ .emit_module(&output[0].module)
+ .context("Unable to emit bundle during Graph2::bundle().")?;
+ }
+
+ String::from_utf8(buf).context("Emitted bundle is an invalid utf-8 string.")
+ }
+
/// Update the handler with any modules that are marked as _dirty_ and update
/// any build info if present.
fn flush(&mut self) -> Result<(), AnyError> {
@@ -963,22 +1121,6 @@ impl Graph2 {
self.modules.get(s)
}
- /// Consume graph and return list of all module specifiers
- /// contained in the graph.
- pub fn get_modules(&self) -> Vec<ModuleSpecifier> {
- self.modules.keys().map(|s| s.to_owned()).collect()
- }
-
- /// Get the source for a given module specifier. If the module is not part
- /// of the graph, the result will be `None`.
- pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option<String> {
- if let Some(module) = self.get_module(specifier) {
- Some(module.source.clone())
- } else {
- None
- }
- }
-
fn get_module_mut(
&mut self,
specifier: &ModuleSpecifier,
@@ -993,6 +1135,41 @@ impl Graph2 {
self.modules.get_mut(s)
}
+ /// Consume graph and return list of all module specifiers contained in the
+ /// graph.
+ pub fn get_modules(&self) -> Vec<ModuleSpecifier> {
+ self.modules.keys().map(|s| s.to_owned()).collect()
+ }
+
+ /// Transform `self.roots` into something that works for `tsc`, because `tsc`
+ /// doesn't like root names without extensions that match its expectations,
+ /// nor does it have any concept of redirection, so we have to resolve all
+ /// that upfront before feeding it to `tsc`.
+ fn get_root_names(&self) -> Vec<(ModuleSpecifier, MediaType)> {
+ self
+ .roots
+ .iter()
+ .map(|ms| {
+ (
+ // root modules can be redirects, so before we pass it to tsc we need
+ // to resolve the redirect
+ self.resolve_specifier(ms).clone(),
+ self.get_media_type(ms).unwrap(),
+ )
+ })
+ .collect()
+ }
+
+ /// Get the source for a given module specifier. If the module is not part
+ /// of the graph, the result will be `None`.
+ pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option<String> {
+ if let Some(module) = self.get_module(specifier) {
+ Some(module.source.clone())
+ } else {
+ None
+ }
+ }
+
/// Return a structure which provides information about the module graph and
/// the relationship of the modules in the graph. This structure is used to
/// provide information for the `info` subcommand.
@@ -1209,7 +1386,7 @@ impl Graph2 {
let maybe_ignored_options =
ts_config.merge_tsconfig(options.maybe_config_path)?;
- let emit_options: EmitOptions = ts_config.clone().into();
+ let emit_options: ast::EmitOptions = ts_config.clone().into();
let mut emit_count: u128 = 0;
let config = ts_config.as_bytes();
@@ -1434,12 +1611,25 @@ impl GraphBuilder2 {
pub mod tests {
use super::*;
+ use crate::specifier_handler::MemoryHandler;
use deno_core::futures::future;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
+ macro_rules! map (
+ { $($key:expr => $value:expr),+ } => {
+ {
+ let mut m = ::std::collections::HashMap::new();
+ $(
+ m.insert($key, $value);
+ )+
+ m
+ }
+ };
+ );
+
/// This is a testing mock for `SpecifierHandler` that uses a special file
/// system renaming to mock local and remote modules as well as provides
/// "spies" for the critical methods for testing purposes.
@@ -1465,20 +1655,7 @@ pub mod tests {
.replace("://", "_")
.replace("/", "-");
let source_path = self.fixtures.join(specifier_text);
- let media_type = match source_path.extension().unwrap().to_str().unwrap()
- {
- "ts" => {
- if source_path.to_string_lossy().ends_with(".d.ts") {
- MediaType::Dts
- } else {
- MediaType::TypeScript
- }
- }
- "tsx" => MediaType::TSX,
- "js" => MediaType::JavaScript,
- "jsx" => MediaType::JSX,
- _ => MediaType::Unknown,
- };
+ let media_type = MediaType::from(&source_path);
let source = fs::read_to_string(&source_path)?;
let is_remote = specifier.as_url().scheme() != "file";
@@ -1572,6 +1749,24 @@ pub mod tests {
(builder.get_graph(), handler)
}
+ async fn setup_memory(
+ specifier: ModuleSpecifier,
+ sources: HashMap<&str, &str>,
+ ) -> Graph2 {
+ let sources: HashMap<String, String> = sources
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+ let handler = Rc::new(RefCell::new(MemoryHandler::new(sources)));
+ let mut builder = GraphBuilder2::new(handler.clone(), None, None);
+ builder
+ .add(&specifier, false)
+ .await
+ .expect("module not inserted");
+
+ builder.get_graph()
+ }
+
#[test]
fn test_get_version() {
let doc_a = "console.log(42);";
@@ -1694,7 +1889,7 @@ pub mod tests {
ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")
.expect("could not resolve module");
let (graph, handler) = setup(specifier).await;
- let (stats, diagnostics, maybe_ignored_options) = graph
+ let result_info = graph
.check(CheckOptions {
debug: false,
emit: true,
@@ -1703,9 +1898,9 @@ pub mod tests {
reload: false,
})
.expect("should have checked");
- assert!(maybe_ignored_options.is_none());
- assert_eq!(stats.0.len(), 12);
- assert!(diagnostics.is_empty());
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert_eq!(result_info.stats.0.len(), 12);
+ assert!(result_info.diagnostics.is_empty());
let h = handler.borrow();
assert_eq!(h.cache_calls.len(), 2);
assert_eq!(h.tsbuildinfo_calls.len(), 1);
@@ -1717,7 +1912,7 @@ pub mod tests {
ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")
.expect("could not resolve module");
let (graph, handler) = setup(specifier).await;
- let (stats, diagnostics, maybe_ignored_options) = graph
+ let result_info = graph
.check(CheckOptions {
debug: false,
emit: false,
@@ -1726,9 +1921,9 @@ pub mod tests {
reload: false,
})
.expect("should have checked");
- assert!(maybe_ignored_options.is_none());
- assert_eq!(stats.0.len(), 12);
- assert!(diagnostics.is_empty());
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert_eq!(result_info.stats.0.len(), 12);
+ assert!(result_info.diagnostics.is_empty());
let h = handler.borrow();
assert_eq!(h.cache_calls.len(), 0);
assert_eq!(h.tsbuildinfo_calls.len(), 1);
@@ -1740,7 +1935,7 @@ pub mod tests {
ModuleSpecifier::resolve_url_or_path("file:///tests/checkwithconfig.ts")
.expect("could not resolve module");
let (graph, handler) = setup(specifier.clone()).await;
- let (_, diagnostics, maybe_ignored_options) = graph
+ let result_info = graph
.check(CheckOptions {
debug: false,
emit: true,
@@ -1751,8 +1946,8 @@ pub mod tests {
reload: true,
})
.expect("should have checked");
- assert!(maybe_ignored_options.is_none());
- assert!(diagnostics.is_empty());
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert!(result_info.diagnostics.is_empty());
let h = handler.borrow();
assert_eq!(h.version_calls.len(), 2);
let ver0 = h.version_calls[0].1.clone();
@@ -1760,7 +1955,7 @@ pub mod tests {
// let's do it all over again to ensure that the versions are determinstic
let (graph, handler) = setup(specifier).await;
- let (_, diagnostics, maybe_ignored_options) = graph
+ let result_info = graph
.check(CheckOptions {
debug: false,
emit: true,
@@ -1771,8 +1966,8 @@ pub mod tests {
reload: true,
})
.expect("should have checked");
- assert!(maybe_ignored_options.is_none());
- assert!(diagnostics.is_empty());
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert!(result_info.diagnostics.is_empty());
let h = handler.borrow();
assert_eq!(h.version_calls.len(), 2);
assert!(h.version_calls[0].1 == ver0 || h.version_calls[0].1 == ver1);
@@ -1780,6 +1975,81 @@ pub mod tests {
}
#[tokio::test]
+ async fn test_graph_emit() {
+ let specifier =
+ ModuleSpecifier::resolve_url_or_path("file:///a.ts").unwrap();
+ let graph = setup_memory(
+ specifier,
+ map!(
+ "/a.ts" => r#"
+ import * as b from "./b.ts";
+
+ console.log(b);
+ "#,
+ "/b.ts" => r#"
+ export const b = "b";
+ "#
+ ),
+ )
+ .await;
+ let (emitted_files, result_info) = graph
+ .emit(EmitOptions {
+ bundle_type: BundleType::None,
+ debug: false,
+ maybe_user_config: None,
+ })
+ .expect("should have emitted");
+ assert!(result_info.diagnostics.is_empty());
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert_eq!(emitted_files.len(), 4);
+ let out_a = emitted_files.get("file:///a.ts.js");
+ assert!(out_a.is_some());
+ let out_a = out_a.unwrap();
+ assert!(out_a.starts_with("import * as b from"));
+ assert!(emitted_files.contains_key("file:///a.ts.js.map"));
+ let out_b = emitted_files.get("file:///b.ts.js");
+ assert!(out_b.is_some());
+ let out_b = out_b.unwrap();
+ assert!(out_b.starts_with("export const b = \"b\";"));
+ assert!(emitted_files.contains_key("file:///b.ts.js.map"));
+ }
+
+ #[tokio::test]
+ async fn test_graph_emit_bundle() {
+ let specifier =
+ ModuleSpecifier::resolve_url_or_path("file:///a.ts").unwrap();
+ let graph = setup_memory(
+ specifier,
+ map!(
+ "/a.ts" => r#"
+ import * as b from "./b.ts";
+
+ console.log(b);
+ "#,
+ "/b.ts" => r#"
+ export const b = "b";
+ "#
+ ),
+ )
+ .await;
+ let (emitted_files, result_info) = graph
+ .emit(EmitOptions {
+ bundle_type: BundleType::Esm,
+ debug: false,
+ maybe_user_config: None,
+ })
+ .expect("should have emitted");
+ assert!(result_info.diagnostics.is_empty());
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert_eq!(emitted_files.len(), 1);
+ let actual = emitted_files.get("deno:///bundle.js");
+ assert!(actual.is_some());
+ let actual = actual.unwrap();
+ assert!(actual.contains("const b = \"b\";"));
+ assert!(actual.contains("console.log(b);"));
+ }
+
+ #[tokio::test]
async fn test_graph_info() {
let specifier =
ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")