diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2020-10-23 11:50:15 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-23 11:50:15 +1100 |
commit | 7e2c7fb6c5454e30158d74e1a5786183ea391f07 (patch) | |
tree | 42402aa26a0422b9c46d1d441598dbe803b8ed15 /cli/module_graph2.rs | |
parent | 9fa59f0ca8164f5e02ba2a2fa90b6fdbce5c1afb (diff) |
refactor(cli): migrate run and cache to new infrastructure (#7996)
Co-authored-by: Ryan Dahl <ry@tinyclouds.org>
Diffstat (limited to 'cli/module_graph2.rs')
-rw-r--r-- | cli/module_graph2.rs | 766 |
1 files changed, 578 insertions, 188 deletions
diff --git a/cli/module_graph2.rs b/cli/module_graph2.rs index e2dcdfefc..678fe8da5 100644 --- a/cli/module_graph2.rs +++ b/cli/module_graph2.rs @@ -6,18 +6,24 @@ use crate::ast::BundleHook; use crate::ast::EmitOptions; use crate::ast::Location; use crate::ast::ParsedModule; +use crate::colors; +use crate::diagnostics::Diagnostics; use crate::import_map::ImportMap; use crate::info::ModuleGraphInfo; use crate::info::ModuleInfo; use crate::info::ModuleInfoMap; use crate::info::ModuleInfoMapItem; +use crate::js; use crate::lockfile::Lockfile; use crate::media_type::MediaType; use crate::specifier_handler::CachedModule; +use crate::specifier_handler::Dependency; 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::tsc_config::IgnoredCompilerOptions; use crate::tsc_config::TsConfig; use crate::version; @@ -26,7 +32,10 @@ use crate::AnyError; use deno_core::error::Context; use deno_core::futures::stream::FuturesUnordered; 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::ModuleResolutionError; use deno_core::ModuleSpecifier; use regex::Regex; use serde::Deserialize; @@ -62,7 +71,6 @@ lazy_static! { /// A group of errors that represent errors that can occur when interacting with /// a module graph. -#[allow(unused)] #[derive(Debug, Clone, Eq, PartialEq)] pub enum GraphError { /// A module using the HTTPS protocol is trying to import a module with an @@ -70,40 +78,37 @@ pub enum GraphError { InvalidDowngrade(ModuleSpecifier, Location), /// A remote module is trying to import a local module. InvalidLocalImport(ModuleSpecifier, Location), - /// A remote module is trying to import a local module. - InvalidSource(ModuleSpecifier, String), - /// A module specifier could not be resolved for a given import. - InvalidSpecifier(String, Location), + /// The source code is invalid, as it does not match the expected hash in the + /// lockfile. + InvalidSource(ModuleSpecifier, PathBuf), /// An unexpected dependency was requested for a module. MissingDependency(ModuleSpecifier, String), /// An unexpected specifier was requested. MissingSpecifier(ModuleSpecifier), - /// Snapshot data was not present in a situation where it was required. - MissingSnapshotData, /// The current feature is not supported. NotSupported(String), + /// A unsupported media type was attempted to be imported as a module. + UnsupportedImportType(ModuleSpecifier, MediaType), } -use GraphError::*; impl fmt::Display for GraphError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - InvalidDowngrade(ref specifier, ref location) => write!(f, "Modules imported via https are not allowed to import http modules.\n Importing: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), - InvalidLocalImport(ref specifier, ref location) => write!(f, "Remote modules are not allowed to import local modules.\n Importing: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), - InvalidSource(ref specifier, ref lockfile) => write!(f, "The source code is invalid, as it does not match the expected hash in the lock file.\n Specifier: {}\n Lock file: {}", specifier, lockfile), - InvalidSpecifier(ref specifier, ref location) => write!(f, "Unable to resolve dependency specifier.\n Specifier: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), - MissingDependency(ref referrer, specifier) => write!( + GraphError::InvalidDowngrade(ref specifier, ref location) => write!(f, "Modules imported via https are not allowed to import http modules.\n Importing: {}\n at {}", specifier, location), + GraphError::InvalidLocalImport(ref specifier, ref location) => write!(f, "Remote modules are not allowed to import local modules. Consider using a dynamic import instead.\n Importing: {}\n at {}", specifier, location), + GraphError::InvalidSource(ref specifier, ref lockfile) => write!(f, "The source code is invalid, as it does not match the expected hash in the lock file.\n Specifier: {}\n Lock file: {}", specifier, lockfile.to_str().unwrap()), + GraphError::MissingDependency(ref referrer, specifier) => write!( f, "The graph is missing a dependency.\n Specifier: {} from {}", specifier, referrer ), - MissingSpecifier(ref specifier) => write!( + GraphError::MissingSpecifier(ref specifier) => write!( f, "The graph is missing a specifier.\n Specifier: {}", specifier ), - MissingSnapshotData => write!(f, "Snapshot data was not supplied, but required."), - NotSupported(ref msg) => write!(f, "{}", msg), + GraphError::NotSupported(ref msg) => write!(f, "{}", msg), + GraphError::UnsupportedImportType(ref specifier, ref media_type) => write!(f, "An unsupported media type was attempted to be imported as a module.\n Specifier: {}\n MediaType: {}", specifier, media_type), } } } @@ -155,7 +160,10 @@ impl swc_bundler::Load for BundleLoader<'_> { self.cm.clone(), ) } else { - Err(MissingDependency(specifier, "<bundle>".to_string()).into()) + Err( + GraphError::MissingDependency(specifier, "<bundle>".to_string()) + .into(), + ) } } _ => unreachable!("Received request for unsupported filename {:?}", file), @@ -252,12 +260,24 @@ impl Default for Module { impl Module { pub fn new( cached_module: CachedModule, + is_root: bool, maybe_import_map: Option<Rc<RefCell<ImportMap>>>, ) -> Self { + // If this is a local root file, and its media type is unknown, set the + // media type to JavaScript. This allows easier ability to create "shell" + // scripts with Deno. + let media_type = if is_root + && !cached_module.is_remote + && cached_module.media_type == MediaType::Unknown + { + MediaType::JavaScript + } else { + cached_module.media_type + }; let mut module = Module { specifier: cached_module.specifier, maybe_import_map, - media_type: cached_module.media_type, + media_type, source: cached_module.source, source_path: cached_module.source_path, maybe_emit: cached_module.maybe_emit, @@ -296,21 +316,28 @@ impl Module { } } + /// Parse a module, populating the structure with data retrieved from the + /// source of the module. pub fn parse(&mut self) -> Result<(), AnyError> { let parsed_module = parse(&self.specifier, &self.source, &self.media_type)?; // parse out any triple slash references for comment in parsed_module.get_leading_comments().iter() { if let Some(ts_reference) = parse_ts_reference(&comment.text) { - let location: Location = parsed_module.get_location(&comment.span); + let location = parsed_module.get_location(&comment.span); match ts_reference { TypeScriptReference::Path(import) => { - let specifier = self.resolve_import(&import, Some(location))?; - let dep = self.dependencies.entry(import).or_default(); + let specifier = + self.resolve_import(&import, Some(location.clone()))?; + let dep = self + .dependencies + .entry(import) + .or_insert_with(|| Dependency::new(location)); dep.maybe_code = Some(specifier); } TypeScriptReference::Types(import) => { - let specifier = self.resolve_import(&import, Some(location))?; + let specifier = + self.resolve_import(&import, Some(location.clone()))?; if self.media_type == MediaType::JavaScript || self.media_type == MediaType::JSX { @@ -318,7 +345,10 @@ impl Module { // this value changes self.maybe_types = Some((import.clone(), specifier)); } else { - let dep = self.dependencies.entry(import).or_default(); + let dep = self + .dependencies + .entry(import) + .or_insert_with(|| Dependency::new(location)); dep.maybe_type = Some(specifier); } } @@ -336,14 +366,30 @@ impl Module { col: desc.col, line: desc.line, }; - let specifier = - self.resolve_import(&desc.specifier, Some(location.clone()))?; + + // In situations where there is a potential issue with resolving the + // import specifier, that ends up being a module resolution error for a + // code dependency, we should not throw in the `ModuleGraph` but instead + // wait until runtime and throw there, as with dynamic imports they need + // to be catchable, which means they need to be resolved at runtime. + let maybe_specifier = + match self.resolve_import(&desc.specifier, Some(location.clone())) { + Ok(specifier) => Some(specifier), + Err(any_error) => { + match any_error.downcast_ref::<ModuleResolutionError>() { + Some(ModuleResolutionError::ImportPrefixMissing(_, _)) => None, + _ => { + return Err(any_error); + } + } + } + }; // Parse out any `@deno-types` pragmas and modify dependency - let maybe_types_specifier = if !desc.leading_comments.is_empty() { + let maybe_type = if !desc.leading_comments.is_empty() { let comment = desc.leading_comments.last().unwrap(); if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() { - Some(self.resolve_import(deno_types, Some(location))?) + Some(self.resolve_import(deno_types, Some(location.clone()))?) } else { None } @@ -354,16 +400,21 @@ impl Module { let dep = self .dependencies .entry(desc.specifier.to_string()) - .or_default(); - if desc.kind == swc_ecmascript::dep_graph::DependencyKind::ExportType - || desc.kind == swc_ecmascript::dep_graph::DependencyKind::ImportType - { - dep.maybe_type = Some(specifier); - } else { - dep.maybe_code = Some(specifier); + .or_insert_with(|| Dependency::new(location)); + dep.is_dynamic = desc.is_dynamic; + if let Some(specifier) = maybe_specifier { + if desc.kind == swc_ecmascript::dep_graph::DependencyKind::ExportType + || desc.kind == swc_ecmascript::dep_graph::DependencyKind::ImportType + { + dep.maybe_type = Some(specifier); + } else { + dep.maybe_code = Some(specifier); + } } - if let Some(types_specifier) = maybe_types_specifier { - dep.maybe_type = Some(types_specifier); + // If the dependency wasn't a type only dependency already, and there is + // a `@deno-types` comment, then we will set the `maybe_type` dependency. + if maybe_type.is_some() && dep.maybe_type.is_none() { + dep.maybe_type = maybe_type; } } @@ -400,14 +451,18 @@ impl Module { // Disallow downgrades from HTTPS to HTTP if referrer_scheme == "https" && specifier_scheme == "http" { - return Err(InvalidDowngrade(specifier.clone(), location).into()); + return Err( + GraphError::InvalidDowngrade(specifier.clone(), location).into(), + ); } // Disallow a remote URL from trying to import a local URL if (referrer_scheme == "https" || referrer_scheme == "http") && !(specifier_scheme == "https" || specifier_scheme == "http") { - return Err(InvalidLocalImport(specifier.clone(), location).into()); + return Err( + GraphError::InvalidLocalImport(specifier.clone(), location).into(), + ); } Ok(specifier) @@ -438,20 +493,71 @@ impl<'de> Deserialize<'de> for Stats { impl fmt::Display for Stats { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Compilation statistics:")?; for (key, value) in self.0.clone() { - write!(f, "{}: {}", key, value)?; + writeln!(f, " {}: {}", key, value)?; } Ok(()) } } +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TypeLib { + DenoWindow, + DenoWorker, + UnstableDenoWindow, + UnstableDenoWorker, +} + +impl Default for TypeLib { + fn default() -> Self { + TypeLib::DenoWindow + } +} + +impl Serialize for TypeLib { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let value = match self { + TypeLib::DenoWindow => vec!["deno.window".to_string()], + TypeLib::DenoWorker => vec!["deno.worker".to_string()], + TypeLib::UnstableDenoWindow => { + vec!["deno.window".to_string(), "deno.unstable".to_string()] + } + TypeLib::UnstableDenoWorker => { + vec!["deno.worker".to_string(), "deno.worker".to_string()] + } + }; + Serialize::serialize(&value, serializer) + } +} + #[derive(Debug, Default)] pub struct BundleOptions { pub debug: bool, pub maybe_config_path: Option<String>, } +#[derive(Debug, Default)] +pub struct CheckOptions { + /// If `true` then debug logging will be output from the isolate. + pub debug: bool, + /// Utilise the emit from `tsc` to update the emitted code for modules. + pub emit: bool, + /// The base type libraries that should be used when type checking. + pub lib: TypeLib, + /// 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>, + /// Ignore any previously emits and ensure that all files are emitted from + /// source. + pub reload: bool, +} + /// A structure which provides options when transpiling modules. #[derive(Debug, Default)] pub struct TranspileOptions { @@ -461,6 +567,9 @@ pub struct TranspileOptions { /// file that augments the the default configuration passed to the TypeScript /// compiler. pub maybe_config_path: Option<String>, + /// Ignore any previously emits and ensure that all files are emitted from + /// source. + pub reload: bool, } /// A dependency graph of modules, were the modules that have been inserted via @@ -468,11 +577,27 @@ pub struct TranspileOptions { /// be able to manipulate and handle the graph. #[derive(Debug)] pub struct Graph2 { + /// A reference to the specifier handler that will retrieve and cache modules + /// for the graph. handler: Rc<RefCell<dyn SpecifierHandler>>, - maybe_ts_build_info: Option<String>, + /// Optional TypeScript build info that will be passed to `tsc` if `tsc` is + /// invoked. + maybe_tsbuildinfo: Option<String>, + /// The modules that are part of the graph. modules: HashMap<ModuleSpecifier, Module>, + /// A map of redirects, where a module specifier is redirected to another + /// module specifier by the handler. All modules references should be + /// resolved internally via this, before attempting to access the module via + /// the handler, to make sure the correct modules is being dealt with. redirects: HashMap<ModuleSpecifier, ModuleSpecifier>, + /// The module specifiers that have been uniquely added to the graph, which + /// does not include any transient dependencies. roots: Vec<ModuleSpecifier>, + /// If all of the root modules are dynamically imported, then this is true. + /// This is used to ensure correct `--reload` behavior, where subsequent + /// calls to a module graph where the emit is already valid do not cause the + /// graph to re-emit. + roots_dynamic: bool, } impl Graph2 { @@ -484,10 +609,11 @@ impl Graph2 { pub fn new(handler: Rc<RefCell<dyn SpecifierHandler>>) -> Self { Graph2 { handler, - maybe_ts_build_info: None, + maybe_tsbuildinfo: None, modules: HashMap::new(), redirects: HashMap::new(), roots: Vec::new(), + roots_dynamic: true, } } @@ -498,7 +624,7 @@ impl Graph2 { options: BundleOptions, ) -> Result<(String, Stats, Option<IgnoredCompilerOptions>), AnyError> { if self.roots.is_empty() || self.roots.len() > 1 { - return Err(NotSupported(format!("Bundling is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); + return Err(GraphError::NotSupported(format!("Bundling is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); } let start = Instant::now(); @@ -566,6 +692,141 @@ impl Graph2 { self.modules.contains_key(s) } + /// Type check the module graph, corresponding to the options provided. + pub fn check( + self, + options: CheckOptions, + ) -> Result<(Stats, Diagnostics, Option<IgnoredCompilerOptions>), AnyError> + { + // TODO(@kitsonk) set to `true` in followup PR + let unstable = options.lib == TypeLib::UnstableDenoWindow + || options.lib == TypeLib::UnstableDenoWorker; + let mut config = TsConfig::new(json!({ + "allowJs": true, + // TODO(@kitsonk) is this really needed? + "esModuleInterop": true, + // Enabled by default to align to transpile/swc defaults + "experimentalDecorators": true, + "incremental": true, + "isolatedModules": unstable, + "lib": options.lib, + "module": "esnext", + "strict": true, + "target": "esnext", + "tsBuildInfoFile": "deno:///.tsbuildinfo", + })); + if options.emit { + config.merge(&json!({ + // TODO(@kitsonk) consider enabling this by default + // see: https://github.com/denoland/deno/issues/7732 + "emitDecoratorMetadata": false, + "jsx": "react", + "inlineSourceMap": true, + "outDir": "deno://", + "removeComments": true, + })); + } else { + config.merge(&json!({ + "noEmit": true, + })); + } + let maybe_ignored_options = + config.merge_user_config(options.maybe_config_path)?; + + // Short circuit if none of the modules require an emit, or all of the + // modules that require an emit have a valid emit. There is also an edge + // case where there are multiple imports of a dynamic module during a + // single invocation, if that is the case, even if there is a reload, we + // will simply look at if the emit is invalid, to avoid two checks for the + // same programme. + if !self.needs_emit(&config) + || (self.is_emit_valid(&config) + && (!options.reload || self.roots_dynamic)) + { + debug!("graph does not need to be checked or emitted."); + return Ok(( + Stats(Vec::new()), + Diagnostics(Vec::new()), + maybe_ignored_options, + )); + } + + // TODO(@kitsonk) not totally happy with this here, but this is the first + // point where we know we are actually going to check the program. If we + // moved it out of here, we wouldn't know until after the check has already + // happened, which isn't informative to the users. + for specifier in &self.roots { + info!("{} {}", colors::green("Check"), specifier); + } + + let root_names: Vec<String> = + self.roots.iter().map(|ms| ms.to_string()).collect(); + 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( + js::compiler_isolate_init(), + Request { + config: config.clone(), + debug: options.debug, + graph: graph.clone(), + hash_data, + maybe_tsbuildinfo, + root_names, + }, + )?; + + let mut graph = graph.borrow_mut(); + graph.maybe_tsbuildinfo = response.maybe_tsbuildinfo; + // Only process changes to the graph if there are no diagnostics and there + // were files emitted. + if response.diagnostics.0.is_empty() && !response.emitted_files.is_empty() { + let mut codes = HashMap::new(); + let mut maps = HashMap::new(); + let check_js = config.get_check_js(); + for emit in &response.emitted_files { + if let Some(specifiers) = &emit.maybe_specifiers { + assert!(specifiers.len() == 1, "Unexpected specifier length"); + let specifier = specifiers[0].clone(); + // Sometimes if tsc sees a CommonJS file it will _helpfully_ output it + // to ESM, which we don't really want unless someone has enabled the + // check_js option. + if !check_js + && graph.get_media_type(&specifier) == Some(MediaType::JavaScript) + { + debug!("skipping emit for {}", specifier); + continue; + } + match emit.media_type { + MediaType::JavaScript => { + codes.insert(specifier, emit.data.clone()); + } + MediaType::SourceMap => { + maps.insert(specifier, emit.data.clone()); + } + _ => unreachable!(), + } + } + } + let config = config.as_bytes(); + for (specifier, code) in codes.iter() { + if let Some(module) = graph.get_module_mut(specifier) { + module.maybe_emit = + Some(Emit::Cli((code.clone(), maps.get(specifier).cloned()))); + module.set_version(&config); + module.is_dirty = true; + } else { + return Err(GraphError::MissingSpecifier(specifier.clone()).into()); + } + } + } + graph.flush()?; + + Ok((response.stats, response.diagnostics, maybe_ignored_options)) + } + /// Update the handler with any modules that are marked as _dirty_ and update /// any build info if present. fn flush(&mut self) -> Result<(), AnyError> { @@ -582,8 +843,8 @@ impl Graph2 { } } for root_specifier in self.roots.iter() { - if let Some(ts_build_info) = &self.maybe_ts_build_info { - handler.set_ts_build_info(root_specifier, ts_build_info.to_owned())?; + if let Some(tsbuildinfo) = &self.maybe_tsbuildinfo { + handler.set_tsbuildinfo(root_specifier, tsbuildinfo.to_owned())?; } } @@ -694,12 +955,26 @@ impl Graph2 { } } + fn get_module_mut( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option<&mut Module> { + // this is duplicated code because `.resolve_specifier` requires an + // immutable borrow, but if `.resolve_specifier` is mut, then everything + // that calls it is is mut + let mut s = specifier; + while let Some(redirect) = self.redirects.get(s) { + s = redirect; + } + self.modules.get_mut(s) + } + /// 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. pub fn info(&self) -> Result<ModuleGraphInfo, AnyError> { if self.roots.is_empty() || self.roots.len() > 1 { - return Err(NotSupported(format!("Info is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); + return Err(GraphError::NotSupported(format!("Info is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); } let module = self.roots[0].clone(); @@ -731,72 +1006,124 @@ impl Graph2 { }) } + /// Determines if all of the modules in the graph that require an emit have + /// a valid emit. Returns `true` if all the modules have a valid emit, + /// otherwise false. + fn is_emit_valid(&self, config: &TsConfig) -> bool { + let check_js = config.get_check_js(); + let config = config.as_bytes(); + self.modules.iter().all(|(_, m)| { + let needs_emit = match m.media_type { + MediaType::TypeScript | MediaType::TSX | MediaType::JSX => true, + MediaType::JavaScript => check_js, + _ => false, + }; + if needs_emit { + m.is_emit_valid(&config) + } else { + true + } + }) + } + /// Verify the subresource integrity of the graph based upon the optional /// lockfile, updating the lockfile with any missing resources. This will /// error if any of the resources do not match their lock status. - pub fn lock( - &self, - maybe_lockfile: &Option<Mutex<Lockfile>>, - ) -> Result<(), AnyError> { + pub fn lock(&self, maybe_lockfile: &Option<Mutex<Lockfile>>) { if let Some(lf) = maybe_lockfile { let mut lockfile = lf.lock().unwrap(); for (ms, module) in self.modules.iter() { let specifier = module.specifier.to_string(); let valid = lockfile.check_or_insert(&specifier, &module.source); if !valid { - return Err( - InvalidSource(ms.clone(), lockfile.filename.display().to_string()) - .into(), + eprintln!( + "{}", + GraphError::InvalidSource(ms.clone(), lockfile.filename.clone()) ); + std::process::exit(10); } } } + } - Ok(()) + /// Determines if any of the modules in the graph are required to be emitted. + /// This is similar to `emit_valid()` except that the actual emit isn't + /// checked to determine if it is valid. + fn needs_emit(&self, config: &TsConfig) -> bool { + let check_js = config.get_check_js(); + self.modules.iter().any(|(_, m)| match m.media_type { + MediaType::TypeScript | MediaType::TSX | MediaType::JSX => true, + MediaType::JavaScript => check_js, + _ => false, + }) } /// Given a string specifier and a referring module specifier, provide the /// resulting module specifier and media type for the module that is part of /// the graph. + /// + /// # Arguments + /// + /// * `specifier` - The string form of the module specifier that needs to be + /// resolved. + /// * `referrer` - The referring `ModuleSpecifier`. + /// * `prefer_types` - When resolving to a module specifier, determine if a + /// type dependency is preferred over a code dependency. This is set to + /// `true` when resolving module names for `tsc` as it needs the type + /// dependency over the code, while other consumers do not handle type only + /// dependencies. pub fn resolve( &self, specifier: &str, referrer: &ModuleSpecifier, + prefer_types: bool, ) -> Result<ModuleSpecifier, AnyError> { if !self.contains_module(referrer) { - return Err(MissingSpecifier(referrer.to_owned()).into()); + return Err(GraphError::MissingSpecifier(referrer.to_owned()).into()); } let module = self.get_module(referrer).unwrap(); if !module.dependencies.contains_key(specifier) { return Err( - MissingDependency(referrer.to_owned(), specifier.to_owned()).into(), + GraphError::MissingDependency( + referrer.to_owned(), + specifier.to_owned(), + ) + .into(), ); } let dependency = module.dependencies.get(specifier).unwrap(); // If there is a @deno-types pragma that impacts the dependency, then the // maybe_type property will be set with that specifier, otherwise we use the // specifier that point to the runtime code. - let resolved_specifier = - if let Some(type_specifier) = dependency.maybe_type.clone() { - type_specifier - } else if let Some(code_specifier) = dependency.maybe_code.clone() { - code_specifier - } else { - return Err( - MissingDependency(referrer.to_owned(), specifier.to_owned()).into(), - ); - }; + let resolved_specifier = if prefer_types && dependency.maybe_type.is_some() + { + dependency.maybe_type.clone().unwrap() + } else if let Some(code_specifier) = dependency.maybe_code.clone() { + code_specifier + } else { + return Err( + GraphError::MissingDependency( + referrer.to_owned(), + specifier.to_owned(), + ) + .into(), + ); + }; if !self.contains_module(&resolved_specifier) { return Err( - MissingDependency(referrer.to_owned(), resolved_specifier.to_string()) - .into(), + GraphError::MissingDependency( + referrer.to_owned(), + resolved_specifier.to_string(), + ) + .into(), ); } let dep_module = self.get_module(&resolved_specifier).unwrap(); // In the case that there is a X-TypeScript-Types or a triple-slash types, // then the `maybe_types` specifier will be populated and we should use that // instead. - let result = if let Some((_, types)) = dep_module.maybe_types.clone() { + let result = if prefer_types && dep_module.maybe_types.is_some() { + let (_, types) = dep_module.maybe_types.clone().unwrap(); types } else { resolved_specifier @@ -835,7 +1162,7 @@ impl Graph2 { /// /// # Arguments /// - /// - `options` - A structure of options which impact how the code is + /// * `options` - A structure of options which impact how the code is /// transpiled. /// pub fn transpile( @@ -858,6 +1185,7 @@ impl Graph2 { let emit_options: EmitOptions = ts_config.clone().into(); let mut emit_count: u128 = 0; + let config = ts_config.as_bytes(); for (_, module) in self.modules.iter_mut() { // TODO(kitsonk) a lot of this logic should be refactored into `Module` as // we start to support other methods on the graph. Especially managing @@ -875,9 +1203,8 @@ impl Graph2 { { continue; } - let config = ts_config.as_bytes(); // skip modules that already have a valid emit - if module.maybe_emit.is_some() && module.is_emit_valid(&config) { + if !options.reload && module.is_emit_valid(&config) { continue; } if module.maybe_parsed_module.is_none() { @@ -917,7 +1244,7 @@ impl swc_bundler::Resolve for Graph2 { referrer ) }; - let specifier = self.resolve(specifier, &referrer)?; + let specifier = self.resolve(specifier, &referrer, false)?; Ok(swc_common::FileName::Custom(specifier.to_string())) } @@ -949,15 +1276,55 @@ impl GraphBuilder2 { } } + /// Add a module into the graph based on a module specifier. The module + /// and any dependencies will be fetched from the handler. The module will + /// also be treated as a _root_ module in the graph. + pub async fn add( + &mut self, + specifier: &ModuleSpecifier, + is_dynamic: bool, + ) -> Result<(), AnyError> { + self.fetch(specifier, &None, is_dynamic)?; + + loop { + let cached_module = self.pending.next().await.unwrap()?; + let is_root = &cached_module.specifier == specifier; + self.visit(cached_module, is_root)?; + if self.pending.is_empty() { + break; + } + } + + if !self.graph.roots.contains(specifier) { + self.graph.roots.push(specifier.clone()); + self.graph.roots_dynamic = self.graph.roots_dynamic && is_dynamic; + if self.graph.maybe_tsbuildinfo.is_none() { + let handler = self.graph.handler.borrow(); + self.graph.maybe_tsbuildinfo = handler.get_tsbuildinfo(specifier)?; + } + } + + Ok(()) + } + /// Request a module to be fetched from the handler and queue up its future /// to be awaited to be resolved. - fn fetch(&mut self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { + fn fetch( + &mut self, + specifier: &ModuleSpecifier, + maybe_referrer: &Option<Location>, + is_dynamic: bool, + ) -> Result<(), AnyError> { if self.fetched.contains(&specifier) { return Ok(()); } self.fetched.insert(specifier.clone()); - let future = self.graph.handler.borrow_mut().fetch(specifier.clone()); + let future = self.graph.handler.borrow_mut().fetch( + specifier.clone(), + maybe_referrer.clone(), + is_dynamic, + ); self.pending.push(future); Ok(()) @@ -966,10 +1333,30 @@ impl GraphBuilder2 { /// Visit a module that has been fetched, hydrating the module, analyzing its /// dependencies if required, fetching those dependencies, and inserting the /// module into the graph. - fn visit(&mut self, cached_module: CachedModule) -> Result<(), AnyError> { + fn visit( + &mut self, + cached_module: CachedModule, + is_root: bool, + ) -> Result<(), AnyError> { let specifier = cached_module.specifier.clone(); let requested_specifier = cached_module.requested_specifier.clone(); - let mut module = Module::new(cached_module, self.maybe_import_map.clone()); + let mut module = + Module::new(cached_module, is_root, self.maybe_import_map.clone()); + match module.media_type { + MediaType::Json + | MediaType::SourceMap + | MediaType::TsBuildInfo + | MediaType::Unknown => { + return Err( + GraphError::UnsupportedImportType( + module.specifier, + module.media_type, + ) + .into(), + ); + } + _ => (), + } if !module.is_parsed { let has_types = module.maybe_types.is_some(); module.parse()?; @@ -984,15 +1371,16 @@ impl GraphBuilder2 { } } for (_, dep) in module.dependencies.iter() { + let maybe_referrer = Some(dep.location.clone()); if let Some(specifier) = dep.maybe_code.as_ref() { - self.fetch(specifier)?; + self.fetch(specifier, &maybe_referrer, dep.is_dynamic)?; } if let Some(specifier) = dep.maybe_type.as_ref() { - self.fetch(specifier)?; + self.fetch(specifier, &maybe_referrer, dep.is_dynamic)?; } } if let Some((_, specifier)) = module.maybe_types.as_ref() { - self.fetch(specifier)?; + self.fetch(specifier, &None, false)?; } if specifier != requested_specifier { self @@ -1005,45 +1393,17 @@ impl GraphBuilder2 { Ok(()) } - /// Insert a module into the graph based on a module specifier. The module - /// and any dependencies will be fetched from the handler. The module will - /// also be treated as a _root_ module in the graph. - pub async fn insert( - &mut self, - specifier: &ModuleSpecifier, - ) -> Result<(), AnyError> { - self.fetch(specifier)?; - - loop { - let cached_module = self.pending.next().await.unwrap()?; - self.visit(cached_module)?; - if self.pending.is_empty() { - break; - } - } - - if !self.graph.roots.contains(specifier) { - self.graph.roots.push(specifier.clone()); - } - - Ok(()) - } - /// Move out the graph from the builder to be utilized further. An optional /// lockfile can be provided, where if the sources in the graph do not match - /// the expected lockfile, the method with error instead of returning the - /// graph. + /// the expected lockfile, an error will be logged and the process will exit. /// /// TODO(@kitsonk) this should really be owned by the graph, but currently /// the lockfile is behind a mutex in program_state, which makes it really /// hard to not pass around as a reference, which if the Graph owned it, it /// would need lifetime parameters and lifetime parameters are 😠- pub fn get_graph( - self, - maybe_lockfile: &Option<Mutex<Lockfile>>, - ) -> Result<Graph2, AnyError> { - self.graph.lock(maybe_lockfile)?; - Ok(self.graph) + pub fn get_graph(self, maybe_lockfile: &Option<Mutex<Lockfile>>) -> Graph2 { + self.graph.lock(maybe_lockfile); + self.graph } } @@ -1063,8 +1423,8 @@ pub mod tests { #[derive(Debug, Default)] pub struct MockSpecifierHandler { pub fixtures: PathBuf, - pub maybe_ts_build_info: Option<String>, - pub ts_build_info_calls: Vec<(ModuleSpecifier, String)>, + pub maybe_tsbuildinfo: Option<String>, + pub tsbuildinfo_calls: Vec<(ModuleSpecifier, String)>, pub cache_calls: Vec<(ModuleSpecifier, Emit)>, pub deps_calls: Vec<(ModuleSpecifier, DependencyMap)>, pub types_calls: Vec<(ModuleSpecifier, String)>, @@ -1097,6 +1457,7 @@ pub mod tests { _ => MediaType::Unknown, }; let source = fs::read_to_string(&source_path)?; + let is_remote = specifier.as_url().scheme() != "file"; Ok(CachedModule { source, @@ -1104,20 +1465,26 @@ pub mod tests { source_path, specifier, media_type, + is_remote, ..CachedModule::default() }) } } impl SpecifierHandler for MockSpecifierHandler { - fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture { + fn fetch( + &mut self, + specifier: ModuleSpecifier, + _maybe_referrer: Option<Location>, + _is_dynamic: bool, + ) -> FetchFuture { Box::pin(future::ready(self.get_cache(specifier))) } - fn get_ts_build_info( + fn get_tsbuildinfo( &self, _specifier: &ModuleSpecifier, ) -> Result<Option<String>, AnyError> { - Ok(self.maybe_ts_build_info.clone()) + Ok(self.maybe_tsbuildinfo.clone()) } fn set_cache( &mut self, @@ -1135,15 +1502,15 @@ pub mod tests { self.types_calls.push((specifier.clone(), types)); Ok(()) } - fn set_ts_build_info( + fn set_tsbuildinfo( &mut self, specifier: &ModuleSpecifier, - ts_build_info: String, + tsbuildinfo: String, ) -> Result<(), AnyError> { - self.maybe_ts_build_info = Some(ts_build_info.clone()); + self.maybe_tsbuildinfo = Some(tsbuildinfo.clone()); self - .ts_build_info_calls - .push((specifier.clone(), ts_build_info)); + .tsbuildinfo_calls + .push((specifier.clone(), tsbuildinfo)); Ok(()) } fn set_deps( @@ -1164,6 +1531,24 @@ pub mod tests { } } + async fn setup( + specifier: ModuleSpecifier, + ) -> (Graph2, Rc<RefCell<MockSpecifierHandler>>) { + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/module_graph"); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures, + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + builder + .add(&specifier, false) + .await + .expect("module not inserted"); + + (builder.get_graph(&None), handler) + } + #[test] fn test_get_version() { let doc_a = "console.log(42);"; @@ -1265,10 +1650,10 @@ pub mod tests { })); let mut builder = GraphBuilder2::new(handler.clone(), None); builder - .insert(&specifier) + .add(&specifier, false) .await .expect("module not inserted"); - let graph = builder.get_graph(&None).expect("could not get graph"); + let graph = builder.get_graph(&None); let (actual, stats, maybe_ignored_options) = graph .bundle(BundleOptions::default()) .expect("could not bundle"); @@ -1281,22 +1666,57 @@ pub mod tests { } #[tokio::test] + async fn test_graph_check_emit() { + let specifier = + 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 + .check(CheckOptions { + debug: false, + emit: true, + lib: TypeLib::DenoWindow, + maybe_config_path: None, + reload: false, + }) + .expect("should have checked"); + assert!(maybe_ignored_options.is_none()); + assert_eq!(stats.0.len(), 12); + assert!(diagnostics.0.is_empty()); + let h = handler.borrow(); + assert_eq!(h.cache_calls.len(), 2); + assert_eq!(h.tsbuildinfo_calls.len(), 1); + } + + #[tokio::test] + async fn test_graph_check_no_emit() { + let specifier = + 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 + .check(CheckOptions { + debug: false, + emit: false, + lib: TypeLib::DenoWindow, + maybe_config_path: None, + reload: false, + }) + .expect("should have checked"); + assert!(maybe_ignored_options.is_none()); + assert_eq!(stats.0.len(), 12); + assert!(diagnostics.0.is_empty()); + let h = handler.borrow(); + assert_eq!(h.cache_calls.len(), 0); + assert_eq!(h.tsbuildinfo_calls.len(), 1); + } + + #[tokio::test] async fn test_graph_info() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder2::new(handler.clone(), None); let specifier = ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - let graph = builder.get_graph(&None).expect("could not get graph"); + let (graph, _) = setup(specifier).await; let info = graph.info().expect("could not get info"); assert!(info.compiled.is_none()); assert_eq!(info.dep_count, 6); @@ -1312,6 +1732,24 @@ pub mod tests { } #[tokio::test] + async fn test_graph_import_json() { + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///tests/importjson.ts") + .expect("could not resolve module"); + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/module_graph"); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures, + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + builder + .add(&specifier, false) + .await + .expect_err("should have errored"); + } + + #[tokio::test] async fn test_graph_transpile() { // This is a complex scenario of transpiling, where we have TypeScript // importing a JavaScript file (with type definitions) which imports @@ -1320,21 +1758,10 @@ pub mod tests { // to be actually emitted. // // This also exercises "@deno-types" and type references. - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder2::new(handler.clone(), None); let specifier = ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - let mut graph = builder.get_graph(&None).expect("could not get graph"); + let (mut graph, handler) = setup(specifier).await; let (stats, maybe_ignored_options) = graph.transpile(TranspileOptions::default()).unwrap(); assert_eq!(stats.0.len(), 3); @@ -1385,25 +1812,15 @@ pub mod tests { #[tokio::test] async fn test_graph_transpile_user_config() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures: fixtures.clone(), - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder2::new(handler.clone(), None); let specifier = ModuleSpecifier::resolve_url_or_path("https://deno.land/x/transpile.tsx") .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - let mut graph = builder.get_graph(&None).expect("could not get graph"); + let (mut graph, handler) = setup(specifier).await; let (_, maybe_ignored_options) = graph .transpile(TranspileOptions { debug: false, maybe_config_path: Some("tests/module_graph/tsconfig.json".to_string()), + reload: false, }) .unwrap(); assert_eq!( @@ -1441,36 +1858,9 @@ pub mod tests { ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") .expect("could not resolve module"); builder - .insert(&specifier) + .add(&specifier, false) .await .expect("module not inserted"); - builder - .get_graph(&maybe_lockfile) - .expect("could not get graph"); - } - - #[tokio::test] - async fn test_graph_with_lockfile_fail() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let lockfile_path = fixtures.join("lockfile_fail.json"); - let lockfile = - Lockfile::new(lockfile_path, false).expect("could not load lockfile"); - let maybe_lockfile = Some(Mutex::new(lockfile)); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder2::new(handler.clone(), None); - let specifier = - ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - builder - .get_graph(&maybe_lockfile) - .expect_err("expected an error"); + builder.get_graph(&maybe_lockfile); } } |