0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-10-30 09:08:00 -04:00
denoland-deno/cli/module_graph.rs
Bartek Iwańczuk 9d63772fe5
refactor: rewrite TS dependency analysis in Rust (#5029)
This commit completely overhauls how module analysis is 
performed in TS compiler by moving the logic to Rust.

In the current setup module analysis is performed using 
"ts.preProcessFile" API in a special TS compiler worker 
running on a separate thread.

"ts.preProcessFile" allowed us to build a lot of functionality
in CLI including X-TypeScript-Types header support 
and @deno-types directive support. Unfortunately at the 
same time complexity of the ops required to perform 
supporting tasks exploded and caused some hidden 
permission escapes.

This PR introduces "ModuleGraphLoader" which can parse
source and load recursively all dependent source files; as 
well as declaration files. All dependencies used in TS 
compiler and now fetched and collected upfront in Rust 
before spinning up TS compiler.

To achieve feature parity with existing APIs this commit 
includes a lot of changes:

* add "ModuleGraphLoader"
  - can fetch local and remote sources
  - parses source code using SWC and extracts imports, exports, file references, special 
     headers
  - this struct inherited all of the hidden complexity and cruft from TS version and requires 
     several follow up PRs
* rewrite cli/tsc.rs to perform module analysis upfront and send all required source code to 
  TS worker in one message
* remove op_resolve_modules and op_fetch_source_files from cli/ops/compiler.rs
* run TS worker on the same thread
2020-05-18 12:59:29 +02:00

732 lines
22 KiB
Rust

// 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);
}
}