summaryrefslogtreecommitdiff
path: root/cli/module_graph.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/module_graph.rs')
-rw-r--r--cli/module_graph.rs732
1 files changed, 732 insertions, 0 deletions
diff --git a/cli/module_graph.rs b/cli/module_graph.rs
new file mode 100644
index 000000000..c21257d82
--- /dev/null
+++ b/cli/module_graph.rs
@@ -0,0 +1,732 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+use crate::file_fetcher::SourceFile;
+use crate::file_fetcher::SourceFileFetcher;
+use crate::import_map::ImportMap;
+use crate::msg::MediaType;
+use crate::op_error::OpError;
+use crate::permissions::Permissions;
+use crate::swc_util::analyze_dependencies_and_references;
+use crate::swc_util::TsReferenceKind;
+use crate::tsc::get_available_libs;
+use deno_core::ErrBox;
+use deno_core::ModuleSpecifier;
+use futures::stream::FuturesUnordered;
+use futures::stream::StreamExt;
+use futures::Future;
+use futures::FutureExt;
+use serde::Serialize;
+use serde::Serializer;
+use std::collections::HashMap;
+use std::hash::BuildHasher;
+use std::pin::Pin;
+
+fn serialize_module_specifier<S>(
+ spec: &ModuleSpecifier,
+ s: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ s.serialize_str(&spec.to_string())
+}
+
+fn serialize_option_module_specifier<S>(
+ maybe_spec: &Option<ModuleSpecifier>,
+ s: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ if let Some(spec) = maybe_spec {
+ serialize_module_specifier(spec, s)
+ } else {
+ s.serialize_none()
+ }
+}
+
+#[derive(Debug, Serialize)]
+pub struct ModuleGraph(HashMap<String, ModuleGraphFile>);
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ImportDescriptor {
+ specifier: String,
+ #[serde(serialize_with = "serialize_module_specifier")]
+ resolved_specifier: ModuleSpecifier,
+ // These two fields are for support of @deno-types directive
+ // directly prepending import statement
+ type_directive: Option<String>,
+ #[serde(serialize_with = "serialize_option_module_specifier")]
+ resolved_type_directive: Option<ModuleSpecifier>,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ReferenceDescriptor {
+ specifier: String,
+ #[serde(serialize_with = "serialize_module_specifier")]
+ resolved_specifier: ModuleSpecifier,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ModuleGraphFile {
+ pub specifier: String,
+ pub url: String,
+ pub filename: String,
+ pub imports: Vec<ImportDescriptor>,
+ pub referenced_files: Vec<ReferenceDescriptor>,
+ pub lib_directives: Vec<ReferenceDescriptor>,
+ pub types_directives: Vec<ReferenceDescriptor>,
+ pub type_headers: Vec<ReferenceDescriptor>,
+ pub media_type: i32,
+ pub source_code: String,
+}
+
+type SourceFileFuture =
+ Pin<Box<dyn Future<Output = Result<SourceFile, ErrBox>>>>;
+
+pub struct ModuleGraphLoader {
+ permissions: Permissions,
+ file_fetcher: SourceFileFetcher,
+ maybe_import_map: Option<ImportMap>,
+ pending_downloads: FuturesUnordered<SourceFileFuture>,
+ pub graph: ModuleGraph,
+ is_dyn_import: bool,
+ analyze_dynamic_imports: bool,
+}
+
+impl ModuleGraphLoader {
+ pub fn new(
+ file_fetcher: SourceFileFetcher,
+ maybe_import_map: Option<ImportMap>,
+ permissions: Permissions,
+ is_dyn_import: bool,
+ analyze_dynamic_imports: bool,
+ ) -> Self {
+ Self {
+ file_fetcher,
+ permissions,
+ maybe_import_map,
+ pending_downloads: FuturesUnordered::new(),
+ graph: ModuleGraph(HashMap::new()),
+ is_dyn_import,
+ analyze_dynamic_imports,
+ }
+ }
+
+ /// This method is used to add specified module and all of its
+ /// dependencies to the graph.
+ ///
+ /// It resolves when all dependent modules have been fetched and analyzed.
+ ///
+ /// This method can be called multiple times.
+ pub async fn add_to_graph(
+ &mut self,
+ specifier: &ModuleSpecifier,
+ ) -> Result<(), ErrBox> {
+ self.download_module(specifier.clone(), None)?;
+
+ loop {
+ let source_file = self.pending_downloads.next().await.unwrap()?;
+ self.visit_module(&source_file.url.clone().into(), source_file)?;
+ if self.pending_downloads.is_empty() {
+ break;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// This method is used to create a graph from in-memory files stored in
+ /// a hash map. Useful for creating module graph for code received from
+ /// the runtime.
+ pub fn build_local_graph<S: BuildHasher>(
+ &mut self,
+ _root_name: &str,
+ source_map: &HashMap<String, String, S>,
+ ) -> Result<(), ErrBox> {
+ for (spec, source_code) in source_map.iter() {
+ self.visit_memory_module(spec.to_string(), source_code.to_string())?;
+ }
+
+ Ok(())
+ }
+
+ /// Consumes the loader and returns created graph.
+ pub fn get_graph(self) -> HashMap<String, ModuleGraphFile> {
+ self.graph.0
+ }
+
+ fn visit_memory_module(
+ &mut self,
+ specifier: String,
+ source_code: String,
+ ) -> Result<(), ErrBox> {
+ let mut imports = vec![];
+ let mut referenced_files = vec![];
+ let mut lib_directives = vec![];
+ let mut types_directives = vec![];
+
+ // FIXME(bartlomieju):
+ // The resolveModules op only handles fully qualified URLs for referrer.
+ // However we will have cases where referrer is "/foo.ts". We add this dummy
+ // prefix "memory://" in order to use resolution logic.
+ let module_specifier =
+ if let Ok(spec) = ModuleSpecifier::resolve_url(&specifier) {
+ spec
+ } else {
+ ModuleSpecifier::resolve_url(&format!("memory://{}", specifier))?
+ };
+
+ let (import_descs, ref_descs) = analyze_dependencies_and_references(
+ &source_code,
+ self.analyze_dynamic_imports,
+ )?;
+
+ for import_desc in import_descs {
+ let maybe_resolved =
+ if let Some(import_map) = self.maybe_import_map.as_ref() {
+ import_map
+ .resolve(&import_desc.specifier, &module_specifier.to_string())?
+ } else {
+ None
+ };
+
+ let resolved_specifier = if let Some(resolved) = maybe_resolved {
+ resolved
+ } else {
+ ModuleSpecifier::resolve_import(
+ &import_desc.specifier,
+ &module_specifier.to_string(),
+ )?
+ };
+
+ let resolved_type_directive =
+ if let Some(types_specifier) = import_desc.deno_types.as_ref() {
+ Some(ModuleSpecifier::resolve_import(
+ &types_specifier,
+ &module_specifier.to_string(),
+ )?)
+ } else {
+ None
+ };
+
+ let import_descriptor = ImportDescriptor {
+ specifier: import_desc.specifier.to_string(),
+ resolved_specifier,
+ type_directive: import_desc.deno_types,
+ resolved_type_directive,
+ };
+
+ imports.push(import_descriptor);
+ }
+
+ let available_libs = get_available_libs();
+
+ for ref_desc in ref_descs {
+ if available_libs.contains(&ref_desc.specifier) {
+ continue;
+ }
+
+ let resolved_specifier = ModuleSpecifier::resolve_import(
+ &ref_desc.specifier,
+ &module_specifier.to_string(),
+ )?;
+
+ let reference_descriptor = ReferenceDescriptor {
+ specifier: ref_desc.specifier.to_string(),
+ resolved_specifier,
+ };
+
+ match ref_desc.kind {
+ TsReferenceKind::Lib => {
+ lib_directives.push(reference_descriptor);
+ }
+ TsReferenceKind::Types => {
+ types_directives.push(reference_descriptor);
+ }
+ TsReferenceKind::Path => {
+ referenced_files.push(reference_descriptor);
+ }
+ }
+ }
+
+ self.graph.0.insert(
+ module_specifier.to_string(),
+ ModuleGraphFile {
+ specifier: specifier.to_string(),
+ url: specifier.to_string(),
+ filename: specifier,
+ // ignored, it's set in TS worker
+ media_type: MediaType::JavaScript as i32,
+ source_code,
+ imports,
+ referenced_files,
+ lib_directives,
+ types_directives,
+ type_headers: vec![],
+ },
+ );
+ Ok(())
+ }
+
+ fn download_module(
+ &mut self,
+ module_specifier: ModuleSpecifier,
+ maybe_referrer: Option<ModuleSpecifier>,
+ ) -> Result<(), ErrBox> {
+ if self.graph.0.contains_key(&module_specifier.to_string()) {
+ return Ok(());
+ }
+
+ if !self.is_dyn_import {
+ // Verify that remote file doesn't try to statically import local file.
+ if let Some(referrer) = maybe_referrer.as_ref() {
+ let referrer_url = referrer.as_url();
+ match referrer_url.scheme() {
+ "http" | "https" => {
+ let specifier_url = module_specifier.as_url();
+ match specifier_url.scheme() {
+ "http" | "https" => {}
+ _ => {
+ let e = OpError::permission_denied("Remote module are not allowed to statically import local modules. Use dynamic import instead.".to_string());
+ return Err(e.into());
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ let spec = module_specifier;
+ let file_fetcher = self.file_fetcher.clone();
+ let perms = self.permissions.clone();
+
+ let load_future = async move {
+ let spec_ = spec.clone();
+ let source_file = file_fetcher
+ .fetch_source_file(&spec_, maybe_referrer, perms)
+ .await?;
+ // FIXME(bartlomieju):
+ // because of redirects we may end up with wrong URL,
+ // substitute with original one
+ Ok(SourceFile {
+ url: spec_.as_url().to_owned(),
+ ..source_file
+ })
+ }
+ .boxed_local();
+
+ self.pending_downloads.push(load_future);
+ Ok(())
+ }
+
+ fn visit_module(
+ &mut self,
+ module_specifier: &ModuleSpecifier,
+ source_file: SourceFile,
+ ) -> Result<(), ErrBox> {
+ let mut imports = vec![];
+ let mut referenced_files = vec![];
+ let mut lib_directives = vec![];
+ let mut types_directives = vec![];
+ let mut type_headers = vec![];
+
+ let source_code = String::from_utf8(source_file.source_code)?;
+
+ if source_file.media_type == MediaType::JavaScript
+ || source_file.media_type == MediaType::TypeScript
+ {
+ if let Some(types_specifier) = source_file.types_header {
+ let type_header = ReferenceDescriptor {
+ specifier: types_specifier.to_string(),
+ resolved_specifier: ModuleSpecifier::resolve_import(
+ &types_specifier,
+ &module_specifier.to_string(),
+ )?,
+ };
+ self.download_module(
+ type_header.resolved_specifier.clone(),
+ Some(module_specifier.clone()),
+ )?;
+ type_headers.push(type_header);
+ }
+
+ let (import_descs, ref_descs) = analyze_dependencies_and_references(
+ &source_code,
+ self.analyze_dynamic_imports,
+ )?;
+
+ for import_desc in import_descs {
+ let maybe_resolved =
+ if let Some(import_map) = self.maybe_import_map.as_ref() {
+ import_map
+ .resolve(&import_desc.specifier, &module_specifier.to_string())?
+ } else {
+ None
+ };
+
+ let resolved_specifier = if let Some(resolved) = maybe_resolved {
+ resolved
+ } else {
+ ModuleSpecifier::resolve_import(
+ &import_desc.specifier,
+ &module_specifier.to_string(),
+ )?
+ };
+
+ let resolved_type_directive =
+ if let Some(types_specifier) = import_desc.deno_types.as_ref() {
+ Some(ModuleSpecifier::resolve_import(
+ &types_specifier,
+ &module_specifier.to_string(),
+ )?)
+ } else {
+ None
+ };
+
+ let import_descriptor = ImportDescriptor {
+ specifier: import_desc.specifier.to_string(),
+ resolved_specifier,
+ type_directive: import_desc.deno_types,
+ resolved_type_directive,
+ };
+
+ self.download_module(
+ import_descriptor.resolved_specifier.clone(),
+ Some(module_specifier.clone()),
+ )?;
+
+ if let Some(type_dir_url) =
+ import_descriptor.resolved_type_directive.as_ref()
+ {
+ self.download_module(
+ type_dir_url.clone(),
+ Some(module_specifier.clone()),
+ )?;
+ }
+
+ imports.push(import_descriptor);
+ }
+
+ let available_libs = get_available_libs();
+
+ for ref_desc in ref_descs {
+ if available_libs.contains(&ref_desc.specifier) {
+ continue;
+ }
+
+ let resolved_specifier = ModuleSpecifier::resolve_import(
+ &ref_desc.specifier,
+ &module_specifier.to_string(),
+ )?;
+
+ let reference_descriptor = ReferenceDescriptor {
+ specifier: ref_desc.specifier.to_string(),
+ resolved_specifier,
+ };
+
+ self.download_module(
+ reference_descriptor.resolved_specifier.clone(),
+ Some(module_specifier.clone()),
+ )?;
+
+ match ref_desc.kind {
+ TsReferenceKind::Lib => {
+ lib_directives.push(reference_descriptor);
+ }
+ TsReferenceKind::Types => {
+ types_directives.push(reference_descriptor);
+ }
+ TsReferenceKind::Path => {
+ referenced_files.push(reference_descriptor);
+ }
+ }
+ }
+ }
+
+ self.graph.0.insert(
+ module_specifier.to_string(),
+ ModuleGraphFile {
+ specifier: module_specifier.to_string(),
+ url: source_file.url.to_string(),
+ filename: source_file.filename.to_str().unwrap().to_string(),
+ media_type: source_file.media_type as i32,
+ source_code,
+ imports,
+ referenced_files,
+ lib_directives,
+ types_directives,
+ type_headers,
+ },
+ );
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::GlobalState;
+
+ async fn build_graph(
+ module_specifier: &ModuleSpecifier,
+ ) -> Result<HashMap<String, ModuleGraphFile>, ErrBox> {
+ let global_state = GlobalState::new(Default::default()).unwrap();
+ let mut graph_loader = ModuleGraphLoader::new(
+ global_state.file_fetcher.clone(),
+ None,
+ Permissions::allow_all(),
+ false,
+ false,
+ );
+ graph_loader.add_to_graph(&module_specifier).await?;
+ Ok(graph_loader.get_graph())
+ }
+
+ #[tokio::test]
+ async fn source_graph_fetch() {
+ let http_server_guard = crate::test_util::http_server();
+
+ let module_specifier = ModuleSpecifier::resolve_url_or_path(
+ "http://localhost:4545/cli/tests/019_media_types.ts",
+ )
+ .unwrap();
+ let graph = build_graph(&module_specifier)
+ .await
+ .expect("Failed to build graph");
+
+ let a = graph
+ .get("http://localhost:4545/cli/tests/019_media_types.ts")
+ .unwrap();
+
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js"
+ ));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/subdir/mt_video_vdn.t2.ts"
+ ));
+ assert!(graph.contains_key("http://localhost:4545/cli/tests/subdir/mt_application_x_typescript.t4.ts"));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/subdir/mt_video_mp2t.t3.ts"
+ ));
+ assert!(graph.contains_key("http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js"));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/subdir/mt_application_ecmascript.j2.js"
+ ));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/subdir/mt_text_javascript.j1.js"
+ ));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/subdir/mt_text_typescript.t1.ts"
+ ));
+
+ assert_eq!(
+ serde_json::to_value(&a.imports).unwrap(),
+ json!([
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_text_typescript.t1.ts",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_text_typescript.t1.ts",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_video_vdn.t2.ts",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_video_vdn.t2.ts",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_video_mp2t.t3.ts",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_video_mp2t.t3.ts",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_typescript.t4.ts",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_typescript.t4.ts",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_text_javascript.j1.js",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_text_javascript.j1.js",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_application_ecmascript.j2.js",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_application_ecmascript.j2.js",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ {
+ "specifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ ])
+ );
+ drop(http_server_guard);
+ }
+
+ #[tokio::test]
+ async fn source_graph_type_references() {
+ let http_server_guard = crate::test_util::http_server();
+
+ let module_specifier = ModuleSpecifier::resolve_url_or_path(
+ "http://localhost:4545/cli/tests/type_definitions.ts",
+ )
+ .unwrap();
+
+ let graph = build_graph(&module_specifier)
+ .await
+ .expect("Failed to build graph");
+
+ eprintln!("json {:#?}", serde_json::to_value(&graph).unwrap());
+
+ let a = graph
+ .get("http://localhost:4545/cli/tests/type_definitions.ts")
+ .unwrap();
+ assert_eq!(
+ serde_json::to_value(&a.imports).unwrap(),
+ json!([
+ {
+ "specifier": "./type_definitions/foo.js",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/type_definitions/foo.js",
+ "typeDirective": "./type_definitions/foo.d.ts",
+ "resolvedTypeDirective": "http://localhost:4545/cli/tests/type_definitions/foo.d.ts"
+ },
+ {
+ "specifier": "./type_definitions/fizz.js",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/type_definitions/fizz.js",
+ "typeDirective": "./type_definitions/fizz.d.ts",
+ "resolvedTypeDirective": "http://localhost:4545/cli/tests/type_definitions/fizz.d.ts"
+ },
+ {
+ "specifier": "./type_definitions/qat.ts",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/type_definitions/qat.ts",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ },
+ ])
+ );
+ assert!(graph
+ .contains_key("http://localhost:4545/cli/tests/type_definitions/foo.js"));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/type_definitions/foo.d.ts"
+ ));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/type_definitions/fizz.js"
+ ));
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/type_definitions/fizz.d.ts"
+ ));
+ assert!(graph
+ .contains_key("http://localhost:4545/cli/tests/type_definitions/qat.ts"));
+
+ drop(http_server_guard);
+ }
+
+ #[tokio::test]
+ async fn source_graph_type_references2() {
+ let http_server_guard = crate::test_util::http_server();
+
+ let module_specifier = ModuleSpecifier::resolve_url_or_path(
+ "http://localhost:4545/cli/tests/type_directives_02.ts",
+ )
+ .unwrap();
+
+ let graph = build_graph(&module_specifier)
+ .await
+ .expect("Failed to build graph");
+
+ eprintln!("{:#?}", serde_json::to_value(&graph).unwrap());
+
+ let a = graph
+ .get("http://localhost:4545/cli/tests/type_directives_02.ts")
+ .unwrap();
+ assert_eq!(
+ serde_json::to_value(&a.imports).unwrap(),
+ json!([
+ {
+ "specifier": "./subdir/type_reference.js",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/type_reference.js",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ }
+ ])
+ );
+
+ assert!(graph.contains_key(
+ "http://localhost:4545/cli/tests/subdir/type_reference.d.ts"
+ ));
+
+ let b = graph
+ .get("http://localhost:4545/cli/tests/subdir/type_reference.js")
+ .unwrap();
+ assert_eq!(
+ serde_json::to_value(&b.types_directives).unwrap(),
+ json!([
+ {
+ "specifier": "./type_reference.d.ts",
+ "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/type_reference.d.ts",
+ }
+ ])
+ );
+ drop(http_server_guard);
+ }
+
+ #[tokio::test]
+ async fn source_graph_type_references3() {
+ let http_server_guard = crate::test_util::http_server();
+
+ let module_specifier = ModuleSpecifier::resolve_url_or_path(
+ "http://localhost:4545/cli/tests/type_directives_01.ts",
+ )
+ .unwrap();
+
+ let graph = build_graph(&module_specifier)
+ .await
+ .expect("Failed to build graph");
+
+ let ts = graph
+ .get("http://localhost:4545/cli/tests/type_directives_01.ts")
+ .unwrap();
+ assert_eq!(
+ serde_json::to_value(&ts.imports).unwrap(),
+ json!([
+ {
+ "specifier": "http://127.0.0.1:4545/xTypeScriptTypes.js",
+ "resolvedSpecifier": "http://127.0.0.1:4545/xTypeScriptTypes.js",
+ "typeDirective": null,
+ "resolvedTypeDirective": null,
+ }
+ ])
+ );
+
+ let headers = graph
+ .get("http://127.0.0.1:4545/xTypeScriptTypes.js")
+ .unwrap();
+ assert_eq!(
+ serde_json::to_value(&headers.type_headers).unwrap(),
+ json!([
+ {
+ "specifier": "./xTypeScriptTypes.d.ts",
+ "resolvedSpecifier": "http://127.0.0.1:4545/xTypeScriptTypes.d.ts"
+ }
+ ])
+ );
+ drop(http_server_guard);
+ }
+}