diff options
Diffstat (limited to 'cli')
-rw-r--r-- | cli/checksum.rs | 23 | ||||
-rw-r--r-- | cli/diagnostics.rs | 30 | ||||
-rw-r--r-- | cli/js.rs | 2 | ||||
-rw-r--r-- | cli/main.rs | 1 | ||||
-rw-r--r-- | cli/media_type.rs | 72 | ||||
-rw-r--r-- | cli/module_graph.rs | 4 | ||||
-rw-r--r-- | cli/module_graph2.rs | 77 | ||||
-rw-r--r-- | cli/tests/tsc2/file_main.ts | 1 | ||||
-rw-r--r-- | cli/tests/tsc2/https_deno.land-x-a.ts | 3 | ||||
-rw-r--r-- | cli/tests/tsc2/https_deno.land-x-b.ts | 1 | ||||
-rw-r--r-- | cli/tests/tsc2/https_deno.land-x-mod.ts | 1 | ||||
-rw-r--r-- | cli/tsc.rs | 4 | ||||
-rw-r--r-- | cli/tsc/99_main_compiler.js | 151 | ||||
-rw-r--r-- | cli/tsc2.rs | 584 |
14 files changed, 874 insertions, 80 deletions
diff --git a/cli/checksum.rs b/cli/checksum.rs index 41e15db2f..a86f527c0 100644 --- a/cli/checksum.rs +++ b/cli/checksum.rs @@ -1,9 +1,12 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -pub fn gen(v: &[&[u8]]) -> String { - let mut ctx = ring::digest::Context::new(&ring::digest::SHA256); +use ring::digest::Context; +use ring::digest::SHA256; + +pub fn gen(v: &[impl AsRef<[u8]>]) -> String { + let mut ctx = Context::new(&SHA256); for src in v { - ctx.update(src); + ctx.update(src.as_ref()); } let digest = ctx.finish(); let out: Vec<String> = digest @@ -13,3 +16,17 @@ pub fn gen(v: &[&[u8]]) -> String { .collect(); out.join("") } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gen() { + let actual = gen(&[b"hello world"]); + assert_eq!( + actual, + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + ); + } +} diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs index b3b5a73c3..290007cc7 100644 --- a/cli/diagnostics.rs +++ b/cli/diagnostics.rs @@ -127,7 +127,7 @@ fn format_message(msg: &str, code: &u64) -> String { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum DiagnosticCategory { Warning, Error, @@ -172,7 +172,7 @@ impl From<i64> for DiagnosticCategory { } } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DiagnosticMessageChain { message_text: String, @@ -199,26 +199,26 @@ impl DiagnosticMessageChain { } } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Position { pub line: u64, pub character: u64, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Diagnostic { - category: DiagnosticCategory, - code: u64, - start: Option<Position>, - end: Option<Position>, - message_text: Option<String>, - message_chain: Option<DiagnosticMessageChain>, - source: Option<String>, - source_line: Option<String>, - file_name: Option<String>, - related_information: Option<Vec<Diagnostic>>, + pub category: DiagnosticCategory, + pub code: u64, + pub start: Option<Position>, + pub end: Option<Position>, + pub message_text: Option<String>, + pub message_chain: Option<DiagnosticMessageChain>, + pub source: Option<String>, + pub source_line: Option<String>, + pub file_name: Option<String>, + pub related_information: Option<Vec<Diagnostic>>, } impl Diagnostic { @@ -346,7 +346,7 @@ impl fmt::Display for Diagnostic { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Diagnostics(pub Vec<Diagnostic>); impl<'de> Deserialize<'de> for Diagnostics { @@ -57,7 +57,7 @@ fn compiler_snapshot() { .execute( "<anon>", r#" - if (!(bootstrapCompilerRuntime)) { + if (!(startup)) { throw Error("bad"); } console.log(`ts version: ${ts.version}`); diff --git a/cli/main.rs b/cli/main.rs index 22dad6d25..ba2b18940 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -51,6 +51,7 @@ mod test_runner; mod text_encoding; mod tokio_util; mod tsc; +pub mod tsc2; mod tsc_config; mod upgrade; mod version; diff --git a/cli/media_type.rs b/cli/media_type.rs index cc3700a66..c3c2f8e23 100644 --- a/cli/media_type.rs +++ b/cli/media_type.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; // Update carefully! #[allow(non_camel_case_types)] #[repr(i32)] -#[derive(Clone, Copy, PartialEq, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum MediaType { JavaScript = 0, JSX = 1, @@ -19,7 +19,9 @@ pub enum MediaType { TSX = 4, Json = 5, Wasm = 6, - Unknown = 8, + TsBuildInfo = 7, + SourceMap = 8, + Unknown = 9, } impl fmt::Display for MediaType { @@ -32,6 +34,8 @@ impl fmt::Display for MediaType { MediaType::TSX => "TSX", MediaType::Json => "Json", MediaType::Wasm => "Wasm", + MediaType::TsBuildInfo => "TsBuildInfo", + MediaType::SourceMap => "SourceMap", MediaType::Unknown => "Unknown", }; write!(f, "{}", value) @@ -56,10 +60,22 @@ impl<'a> From<&'a String> for MediaType { } } +impl Default for MediaType { + fn default() -> Self { + MediaType::Unknown + } +} + impl MediaType { fn from_path(path: &Path) -> Self { match path.extension() { - None => MediaType::Unknown, + None => match path.file_name() { + None => MediaType::Unknown, + Some(os_str) => match os_str.to_str() { + Some(".tsbuildinfo") => MediaType::TsBuildInfo, + _ => MediaType::Unknown, + }, + }, Some(os_str) => match os_str.to_str() { Some("ts") => MediaType::TypeScript, Some("tsx") => MediaType::TSX, @@ -69,10 +85,42 @@ impl MediaType { Some("cjs") => MediaType::JavaScript, Some("json") => MediaType::Json, Some("wasm") => MediaType::Wasm, + Some("tsbuildinfo") => MediaType::TsBuildInfo, + Some("map") => MediaType::SourceMap, _ => MediaType::Unknown, }, } } + + /// Convert a MediaType to a `ts.Extension`. + /// + /// *NOTE* This is defined in TypeScript as a string based enum. Changes to + /// that enum in TypeScript should be reflected here. + pub fn as_ts_extension(&self) -> String { + let ext = match self { + MediaType::JavaScript => ".js", + MediaType::JSX => ".jsx", + MediaType::TypeScript => ".ts", + MediaType::Dts => ".d.ts", + MediaType::TSX => ".tsx", + MediaType::Json => ".json", + // TypeScript doesn't have an "unknown", so we will treat WASM as JS for + // mapping purposes, though in reality, it is unlikely to ever be passed + // to the compiler. + MediaType::Wasm => ".js", + MediaType::TsBuildInfo => ".tsbuildinfo", + // TypeScript doesn't have an "source map", so we will treat SourceMap as + // JS for mapping purposes, though in reality, it is unlikely to ever be + // passed to the compiler. + MediaType::SourceMap => ".js", + // TypeScript doesn't have an "unknown", so we will treat WASM as JS for + // mapping purposes, though in reality, it is unlikely to ever be passed + // to the compiler. + MediaType::Unknown => ".js", + }; + + ext.into() + } } impl Serialize for MediaType { @@ -88,7 +136,9 @@ impl Serialize for MediaType { MediaType::TSX => 4 as i32, MediaType::Json => 5 as i32, MediaType::Wasm => 6 as i32, - MediaType::Unknown => 8 as i32, + MediaType::TsBuildInfo => 7 as i32, + MediaType::SourceMap => 8 as i32, + MediaType::Unknown => 9 as i32, }; Serialize::serialize(&value, serializer) } @@ -133,6 +183,14 @@ mod tests { MediaType::JavaScript ); assert_eq!( + MediaType::from(Path::new("foo/.tsbuildinfo")), + MediaType::TsBuildInfo + ); + assert_eq!( + MediaType::from(Path::new("foo/bar.js.map")), + MediaType::SourceMap + ); + assert_eq!( MediaType::from(Path::new("foo/bar.txt")), MediaType::Unknown ); @@ -148,7 +206,9 @@ mod tests { assert_eq!(json!(MediaType::TSX), json!(4)); assert_eq!(json!(MediaType::Json), json!(5)); assert_eq!(json!(MediaType::Wasm), json!(6)); - assert_eq!(json!(MediaType::Unknown), json!(8)); + assert_eq!(json!(MediaType::TsBuildInfo), json!(7)); + assert_eq!(json!(MediaType::SourceMap), json!(8)); + assert_eq!(json!(MediaType::Unknown), json!(9)); } #[test] @@ -160,6 +220,8 @@ mod tests { assert_eq!(format!("{}", MediaType::TSX), "TSX"); assert_eq!(format!("{}", MediaType::Json), "Json"); assert_eq!(format!("{}", MediaType::Wasm), "Wasm"); + assert_eq!(format!("{}", MediaType::TsBuildInfo), "TsBuildInfo"); + assert_eq!(format!("{}", MediaType::SourceMap), "SourceMap"); assert_eq!(format!("{}", MediaType::Unknown), "Unknown"); } } diff --git a/cli/module_graph.rs b/cli/module_graph.rs index 22b629c1a..f0645424e 100644 --- a/cli/module_graph.rs +++ b/cli/module_graph.rs @@ -465,7 +465,7 @@ impl ModuleGraphLoader { filename: source_file.filename.to_str().unwrap().to_string(), version_hash: checksum::gen(&[ &source_file.source_code.as_bytes(), - version::DENO.as_bytes(), + &version::DENO.as_bytes(), ]), media_type: source_file.media_type, source_code: "".to_string(), @@ -481,7 +481,7 @@ impl ModuleGraphLoader { let module_specifier = ModuleSpecifier::from(source_file.url.clone()); let version_hash = checksum::gen(&[ &source_file.source_code.as_bytes(), - version::DENO.as_bytes(), + &version::DENO.as_bytes(), ]); let source_code = source_file.source_code.clone(); diff --git a/cli/module_graph2.rs b/cli/module_graph2.rs index 412519178..de9e3a5db 100644 --- a/cli/module_graph2.rs +++ b/cli/module_graph2.rs @@ -373,8 +373,8 @@ impl Module { } } -#[derive(Clone, Debug, PartialEq)] -pub struct Stats(Vec<(String, u128)>); +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Stats(pub Vec<(String, u128)>); impl<'de> Deserialize<'de> for Stats { fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error> @@ -572,6 +572,27 @@ impl Graph2 { Ok(()) } + pub fn get_media_type( + &self, + specifier: &ModuleSpecifier, + ) -> Option<MediaType> { + if let Some(module) = self.modules.get(specifier) { + Some(module.media_type) + } else { + None + } + } + + /// 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.modules.get(specifier) { + Some(module.source.clone()) + } else { + None + } + } + /// 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. @@ -595,6 +616,56 @@ impl Graph2 { Ok(()) } + /// 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. + pub fn resolve( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + ) -> Result<ModuleSpecifier, AnyError> { + if !self.modules.contains_key(referrer) { + return Err(MissingSpecifier(referrer.to_owned()).into()); + } + let module = self.modules.get(referrer).unwrap(); + if !module.dependencies.contains_key(specifier) { + return Err( + 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(), + ); + }; + if !self.modules.contains_key(&resolved_specifier) { + return Err( + MissingDependency(referrer.to_owned(), resolved_specifier.to_string()) + .into(), + ); + } + let dep_module = self.modules.get(&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() { + types + } else { + resolved_specifier + }; + + Ok(result) + } + /// Transpile (only transform) the graph, updating any emitted modules /// with the specifier handler. The result contains any performance stats /// from the compiler and optionally any user provided configuration compiler @@ -798,7 +869,7 @@ impl GraphBuilder2 { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use deno_core::futures::future; diff --git a/cli/tests/tsc2/file_main.ts b/cli/tests/tsc2/file_main.ts new file mode 100644 index 000000000..a45477fde --- /dev/null +++ b/cli/tests/tsc2/file_main.ts @@ -0,0 +1 @@ +console.log("hello deno"); diff --git a/cli/tests/tsc2/https_deno.land-x-a.ts b/cli/tests/tsc2/https_deno.land-x-a.ts new file mode 100644 index 000000000..72b3a67bc --- /dev/null +++ b/cli/tests/tsc2/https_deno.land-x-a.ts @@ -0,0 +1,3 @@ +import * as b from "./b.ts"; + +console.log(b); diff --git a/cli/tests/tsc2/https_deno.land-x-b.ts b/cli/tests/tsc2/https_deno.land-x-b.ts new file mode 100644 index 000000000..59d168993 --- /dev/null +++ b/cli/tests/tsc2/https_deno.land-x-b.ts @@ -0,0 +1 @@ +export const b = "b"; diff --git a/cli/tests/tsc2/https_deno.land-x-mod.ts b/cli/tests/tsc2/https_deno.land-x-mod.ts new file mode 100644 index 000000000..a45477fde --- /dev/null +++ b/cli/tests/tsc2/https_deno.land-x-mod.ts @@ -0,0 +1 @@ +console.log("hello deno"); diff --git a/cli/tsc.rs b/cli/tsc.rs index 24511fe6a..02ca9d59e 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -417,7 +417,7 @@ impl TsCompiler { { let existing_hash = crate::checksum::gen(&[ &source_file.source_code.as_bytes(), - version::DENO.as_bytes(), + &version::DENO.as_bytes(), ]); let expected_hash = file_info["version"].as_str().unwrap().to_string(); @@ -988,7 +988,7 @@ fn execute_in_tsc( } let bootstrap_script = format!( - "globalThis.bootstrapCompilerRuntime({{ debugFlag: {} }})", + "globalThis.startup({{ debugFlag: {}, legacy: true }})", debug_flag ); js_runtime.execute("<compiler>", &bootstrap_script)?; diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 7bb0c6c92..4bef1bd8b 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -4,7 +4,7 @@ // that is created when Deno needs to compile TS/WASM to JS. // // It provides two functions that should be called by Rust: -// - `bootstrapCompilerRuntime` +// - `startup` // This functions must be called when creating isolate // to properly setup runtime. // - `tsCompilerOnMessage` @@ -54,6 +54,9 @@ delete Object.prototype.__proto__; } } + /** @type {Map<string, ts.SourceFile>} */ + const sourceFileCache = new Map(); + /** * @param {import("../dts/typescript").DiagnosticRelatedInformation} diagnostic */ @@ -296,15 +299,15 @@ delete Object.prototype.__proto__; debug(`host.fileExists("${fileName}")`); return false; }, - readFile(fileName) { - debug(`host.readFile("${fileName}")`); + readFile(specifier) { + debug(`host.readFile("${specifier}")`); if (legacy) { - if (fileName == TS_BUILD_INFO) { + if (specifier == TS_BUILD_INFO) { return legacyHostState.buildInfo; } return unreachable(); } else { - return core.jsonOpSync("op_read_file", { fileName }).data; + return core.jsonOpSync("op_load", { specifier }).data; } }, getSourceFile( @@ -338,6 +341,14 @@ delete Object.prototype.__proto__; ); sourceFile.tsSourceFile.version = sourceFile.versionHash; delete sourceFile.sourceCode; + + // This code is to support transition from the "legacy" compiler + // to the new one, by populating the new source file cache. + if ( + !sourceFileCache.has(specifier) && specifier.startsWith(ASSETS) + ) { + sourceFileCache.set(specifier, sourceFile.tsSourceFile); + } } return sourceFile.tsSourceFile; } catch (e) { @@ -349,37 +360,26 @@ delete Object.prototype.__proto__; return undefined; } } else { - const sourceFile = sourceFileCache.get(specifier); + let sourceFile = sourceFileCache.get(specifier); if (sourceFile) { return sourceFile; } - try { - /** @type {{ data: string; hash: string; }} */ - const { data, hash } = core.jsonOpSync( - "op_load_module", - { specifier }, - ); - const sourceFile = ts.createSourceFile( - specifier, - data, - languageVersion, - ); - sourceFile.moduleName = specifier; - sourceFile.version = hash; - sourceFileCache.set(specifier, sourceFile); - return sourceFile; - } catch (err) { - const message = err instanceof Error - ? err.message - : JSON.stringify(err); - debug(` !! error: ${message}`); - if (onError) { - onError(message); - } else { - throw err; - } - } + /** @type {{ data: string; hash: string; }} */ + const { data, hash } = core.jsonOpSync( + "op_load", + { specifier }, + ); + assert(data, `"data" is unexpectedly null for "${specifier}".`); + sourceFile = ts.createSourceFile( + specifier, + data, + languageVersion, + ); + sourceFile.moduleName = specifier; + sourceFile.version = hash; + sourceFileCache.set(specifier, sourceFile); + return sourceFile; } }, getDefaultLibFileName() { @@ -392,7 +392,7 @@ delete Object.prototype.__proto__; return `${ASSETS}/lib.deno.worker.d.ts`; } } else { - return `lib.esnext.d.ts`; + return `${ASSETS}/lib.esnext.d.ts`; } }, getDefaultLibLocation() { @@ -403,16 +403,14 @@ delete Object.prototype.__proto__; if (legacy) { legacyHostState.writeFile(fileName, data, sourceFiles); } else { - let maybeModuleName; + let maybeSpecifiers; if (sourceFiles) { - assert(sourceFiles.length === 1, "unexpected number of source files"); - const [sourceFile] = sourceFiles; - maybeModuleName = sourceFile.moduleName; - debug(` moduleName: ${maybeModuleName}`); + maybeSpecifiers = sourceFiles.map((sf) => sf.moduleName); + debug(` specifiers: ${maybeSpecifiers.join(", ")}`); } return core.jsonOpSync( - "op_write_file", - { maybeModuleName, fileName, data }, + "op_emit", + { maybeSpecifiers, fileName, data }, ); } }, @@ -463,7 +461,7 @@ delete Object.prototype.__proto__; return resolved; } else { /** @type {Array<[string, import("../dts/typescript").Extension]>} */ - const resolved = core.jsonOpSync("op_resolve_specifiers", { + const resolved = core.jsonOpSync("op_resolve", { specifiers, base, }); @@ -737,6 +735,7 @@ delete Object.prototype.__proto__; 1208, ]; + /** @type {Array<{ key: string, value: number }>} */ const stats = []; let statsStart = 0; @@ -779,7 +778,6 @@ delete Object.prototype.__proto__; } function performanceEnd() { - // TODO(kitsonk) replace with performance.measure() when landed const duration = new Date() - statsStart; stats.push({ key: "Compile time", value: duration }); return stats; @@ -1328,18 +1326,73 @@ delete Object.prototype.__proto__; } } - let hasBootstrapped = false; + /** + * @typedef {object} Request + * @property {Record<string, any>} config + * @property {boolean} debug + * @property {string[]} rootNames + */ - function bootstrapCompilerRuntime({ debugFlag }) { - if (hasBootstrapped) { - throw new Error("Worker runtime already bootstrapped"); + /** The API that is called by Rust when executing a request. + * @param {Request} request + */ + function exec({ config, debug: debugFlag, rootNames }) { + setLogDebug(debugFlag, "TS"); + performanceStart(); + debug(">>> exec start", { rootNames }); + debug(config); + + const { options, errors: configFileParsingDiagnostics } = ts + .convertCompilerOptionsFromJson(config, "", "tsconfig.json"); + const program = ts.createIncrementalProgram({ + rootNames, + options, + host, + configFileParsingDiagnostics, + }); + + const { diagnostics: emitDiagnostics } = program.emit(); + + const diagnostics = [ + ...program.getConfigFileParsingDiagnostics(), + ...program.getSyntacticDiagnostics(), + ...program.getOptionsDiagnostics(), + ...program.getGlobalDiagnostics(), + ...program.getSemanticDiagnostics(), + ...emitDiagnostics, + ].filter(({ code }) => + !IGNORED_DIAGNOSTICS.includes(code) && + !IGNORED_COMPILE_DIAGNOSTICS.includes(code) + ); + performanceProgram({ program }); + + // TODO(@kitsonk) when legacy stats are removed, convert to just tuples + let stats = performanceEnd().map(({ key, value }) => [key, value]); + core.jsonOpSync("op_respond", { + diagnostics: fromTypeScriptDiagnostic(diagnostics), + stats, + }); + debug("<<< exec stop"); + } + + let hasStarted = false; + + /** Startup the runtime environment, setting various flags. + * @param {{ debugFlag?: boolean; legacyFlag?: boolean; }} msg + */ + function startup({ debugFlag = false, legacyFlag = true }) { + if (hasStarted) { + throw new Error("The compiler runtime already started."); } - hasBootstrapped = true; - delete globalThis.__bootstrap; + hasStarted = true; core.ops(); + core.registerErrorClass("Error", Error); setLogDebug(!!debugFlag, "TS"); + legacy = legacyFlag; } - globalThis.bootstrapCompilerRuntime = bootstrapCompilerRuntime; + globalThis.startup = startup; + globalThis.exec = exec; + // TODO(@kitsonk) remove when converted from legacy tsc globalThis.tsCompilerOnMessage = tsCompilerOnMessage; })(this); diff --git a/cli/tsc2.rs b/cli/tsc2.rs new file mode 100644 index 000000000..64563ce01 --- /dev/null +++ b/cli/tsc2.rs @@ -0,0 +1,584 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::diagnostics::Diagnostics; +use crate::media_type::MediaType; +use crate::module_graph2::Graph2; +use crate::module_graph2::Stats; +use crate::tsc_config::TsConfig; + +use deno_core::error::anyhow; +use deno_core::error::bail; +use deno_core::error::AnyError; +use deno_core::error::Context; +use deno_core::json_op_sync; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::JsRuntime; +use deno_core::ModuleSpecifier; +use deno_core::OpFn; +use deno_core::RuntimeOptions; +use deno_core::Snapshot; +use serde::Deserialize; +use serde::Serialize; +use std::rc::Rc; + +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct EmittedFile { + pub data: String, + pub maybe_specifiers: Option<Vec<ModuleSpecifier>>, + pub media_type: MediaType, +} + +/// A structure representing a request to be sent to the tsc runtime. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Request { + /// The TypeScript compiler options which will be serialized and sent to + /// tsc. + pub config: TsConfig, + /// Indicates to the tsc runtime if debug logging should occur. + pub debug: bool, + #[serde(skip_serializing)] + pub graph: Rc<Graph2>, + #[serde(skip_serializing)] + pub hash_data: Vec<Vec<u8>>, + #[serde(skip_serializing)] + pub maybe_tsbuildinfo: Option<String>, + /// A vector of strings that represent the root/entry point modules for the + /// program. + pub root_names: Vec<String>, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Response { + /// Any diagnostics that have been returned from the checker. + pub diagnostics: Diagnostics, + /// Any files that were emitted during the check. + pub emitted_files: Vec<EmittedFile>, + /// If there was any build info associated with the exec request. + pub maybe_tsbuildinfo: Option<String>, + /// Statistics from the check. + pub stats: Stats, +} + +struct State { + hash_data: Vec<Vec<u8>>, + emitted_files: Vec<EmittedFile>, + graph: Rc<Graph2>, + maybe_tsbuildinfo: Option<String>, + maybe_response: Option<RespondArgs>, +} + +impl State { + pub fn new( + graph: Rc<Graph2>, + hash_data: Vec<Vec<u8>>, + maybe_tsbuildinfo: Option<String>, + ) -> Self { + State { + hash_data, + emitted_files: Vec::new(), + graph, + maybe_tsbuildinfo, + maybe_response: None, + } + } +} + +fn op<F>(op_fn: F) -> Box<OpFn> +where + F: Fn(&mut State, Value) -> Result<Value, AnyError> + 'static, +{ + json_op_sync(move |s, args, _bufs| { + let state = s.borrow_mut::<State>(); + op_fn(state, args) + }) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateHashArgs { + /// The string data to be used to generate the hash. This will be mixed with + /// other state data in Deno to derive the final hash. + data: String, +} + +fn create_hash(state: &mut State, args: Value) -> Result<Value, AnyError> { + let v: CreateHashArgs = serde_json::from_value(args) + .context("Invalid request from JavaScript for \"op_create_hash\".")?; + let mut data = vec![v.data.as_bytes().to_owned()]; + data.extend_from_slice(&state.hash_data); + let hash = crate::checksum::gen(&data); + Ok(json!({ "hash": hash })) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct EmitArgs { + /// The text data/contents of the file. + data: String, + /// The _internal_ filename for the file. This will be used to determine how + /// the file is cached and stored. + file_name: String, + /// A string representation of the specifier that was associated with a + /// module. This should be present on every module that represents a module + /// that was requested to be transformed. + maybe_specifiers: Option<Vec<String>>, +} + +fn emit(state: &mut State, args: Value) -> Result<Value, AnyError> { + let v: EmitArgs = serde_json::from_value(args) + .context("Invalid request from JavaScript for \"op_emit\".")?; + match v.file_name.as_ref() { + "deno:///.tsbuildinfo" => state.maybe_tsbuildinfo = Some(v.data), + _ => state.emitted_files.push(EmittedFile { + data: v.data, + maybe_specifiers: if let Some(specifiers) = &v.maybe_specifiers { + let specifiers = specifiers + .iter() + .map(|s| ModuleSpecifier::resolve_url_or_path(s).unwrap()) + .collect(); + Some(specifiers) + } else { + None + }, + media_type: MediaType::from(&v.file_name), + }), + } + + Ok(json!(true)) +} + +#[derive(Debug, Deserialize)] +struct LoadArgs { + /// The fully qualified specifier that should be loaded. + specifier: String, +} + +fn load(state: &mut State, args: Value) -> Result<Value, AnyError> { + let v: LoadArgs = serde_json::from_value(args) + .context("Invalid request from JavaScript for \"op_load\".")?; + let specifier = ModuleSpecifier::resolve_url_or_path(&v.specifier) + .context("Error converting a string module specifier for \"op_load\".")?; + let mut hash: Option<String> = None; + let data = if &v.specifier == "deno:///.tsbuildinfo" { + state.maybe_tsbuildinfo.clone() + } else { + let maybe_source = state.graph.get_source(&specifier); + if let Some(source) = &maybe_source { + let mut data = vec![source.as_bytes().to_owned()]; + data.extend_from_slice(&state.hash_data); + hash = Some(crate::checksum::gen(&data)); + } + maybe_source + }; + + Ok(json!({ "data": data, "hash": hash })) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ResolveArgs { + /// The base specifier that the supplied specifier strings should be resolved + /// relative to. + base: String, + /// A list of specifiers that should be resolved. + specifiers: Vec<String>, +} + +fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { + let v: ResolveArgs = serde_json::from_value(args) + .context("Invalid request from JavaScript for \"op_resolve\".")?; + let mut resolved: Vec<(String, String)> = Vec::new(); + let referrer = ModuleSpecifier::resolve_url_or_path(&v.base).context( + "Error converting a string module specifier for \"op_resolve\".", + )?; + for specifier in &v.specifiers { + if specifier.starts_with("asset:///") { + resolved.push(( + specifier.clone(), + MediaType::from(specifier).as_ts_extension().to_string(), + )); + } else { + let resolved_specifier = state.graph.resolve(specifier, &referrer)?; + let media_type = if let Some(media_type) = + state.graph.get_media_type(&resolved_specifier) + { + media_type + } else { + bail!( + "Unable to resolve media type for specifier: \"{}\"", + resolved_specifier + ) + }; + resolved + .push((resolved_specifier.to_string(), media_type.as_ts_extension())); + } + } + + Ok(json!(resolved)) +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct RespondArgs { + pub diagnostics: Diagnostics, + pub stats: Stats, +} + +fn respond(state: &mut State, args: Value) -> Result<Value, AnyError> { + let v: RespondArgs = serde_json::from_value(args) + .context("Error converting the result for \"op_respond\".")?; + state.maybe_response = Some(v); + Ok(json!(true)) +} + +/// Execute a request on the supplied snapshot, returning a response which +/// contains information, like any emitted files, diagnostics, statistics and +/// optionally an updated TypeScript build info. +pub fn exec( + snapshot: Snapshot, + request: Request, +) -> Result<Response, AnyError> { + let mut runtime = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(snapshot), + ..Default::default() + }); + + { + let op_state = runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + op_state.put(State::new( + request.graph.clone(), + request.hash_data.clone(), + request.maybe_tsbuildinfo.clone(), + )); + } + + runtime.register_op("op_create_hash", op(create_hash)); + runtime.register_op("op_emit", op(emit)); + runtime.register_op("op_load", op(load)); + runtime.register_op("op_resolve", op(resolve)); + runtime.register_op("op_respond", op(respond)); + + let startup_source = "globalThis.startup({ legacyFlag: false })"; + let request_str = + serde_json::to_string(&request).context("Could not serialize request.")?; + let exec_source = format!("globalThis.exec({})", request_str); + + runtime + .execute("[native code]", startup_source) + .context("Could not properly start the compiler runtime.")?; + runtime + .execute("[native_code]", &exec_source) + .context("Execute request failed.")?; + + let op_state = runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + let state = op_state.take::<State>(); + + if let Some(response) = state.maybe_response { + let diagnostics = response.diagnostics; + let emitted_files = state.emitted_files; + let maybe_tsbuildinfo = state.maybe_tsbuildinfo; + let stats = response.stats; + + Ok(Response { + diagnostics, + emitted_files, + maybe_tsbuildinfo, + stats, + }) + } else { + Err(anyhow!("The response for the exec request was not set.")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::diagnostics::Diagnostic; + use crate::diagnostics::DiagnosticCategory; + use crate::js; + use crate::module_graph2::tests::MockSpecifierHandler; + use crate::module_graph2::GraphBuilder2; + use crate::tsc_config::TsConfig; + use std::cell::RefCell; + use std::env; + use std::path::PathBuf; + + async fn setup( + maybe_specifier: Option<ModuleSpecifier>, + maybe_hash_data: Option<Vec<Vec<u8>>>, + maybe_tsbuildinfo: Option<String>, + ) -> State { + let specifier = maybe_specifier.unwrap_or_else(|| { + ModuleSpecifier::resolve_url_or_path("file:///main.ts").unwrap() + }); + let hash_data = maybe_hash_data.unwrap_or_else(|| vec![b"".to_vec()]); + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/tsc2"); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures, + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + builder + .insert(&specifier) + .await + .expect("module not inserted"); + let graph = Rc::new(builder.get_graph(&None).expect("could not get graph")); + State::new(graph, hash_data, maybe_tsbuildinfo) + } + + #[tokio::test] + async fn test_create_hash() { + let mut state = setup(None, Some(vec![b"something".to_vec()]), None).await; + let actual = + create_hash(&mut state, json!({ "data": "some sort of content" })) + .expect("could not invoke op"); + assert_eq!( + actual, + json!({"hash": "ae92df8f104748768838916857a1623b6a3c593110131b0a00f81ad9dac16511"}) + ); + } + + #[tokio::test] + async fn test_emit() { + let mut state = setup(None, None, None).await; + let actual = emit( + &mut state, + json!({ + "data": "some file content", + "fileName": "cache:///some/file.js", + "maybeSpecifiers": ["file:///some/file.ts"] + }), + ) + .expect("should have invoked op"); + assert_eq!(actual, json!(true)); + assert_eq!(state.emitted_files.len(), 1); + assert!(state.maybe_tsbuildinfo.is_none()); + assert_eq!( + state.emitted_files[0], + EmittedFile { + data: "some file content".to_string(), + maybe_specifiers: Some(vec![ModuleSpecifier::resolve_url_or_path( + "file:///some/file.ts" + ) + .unwrap()]), + media_type: MediaType::JavaScript, + } + ); + } + + #[tokio::test] + async fn test_emit_tsbuildinfo() { + let mut state = setup(None, None, None).await; + let actual = emit( + &mut state, + json!({ + "data": "some file content", + "fileName": "deno:///.tsbuildinfo", + }), + ) + .expect("should have invoked op"); + assert_eq!(actual, json!(true)); + assert_eq!(state.emitted_files.len(), 0); + assert_eq!( + state.maybe_tsbuildinfo, + Some("some file content".to_string()) + ); + } + + #[tokio::test] + async fn test_load() { + let mut state = setup( + Some( + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/mod.ts") + .unwrap(), + ), + None, + Some("some content".to_string()), + ) + .await; + let actual = load( + &mut state, + json!({ "specifier": "https://deno.land/x/mod.ts"}), + ) + .expect("should have invoked op"); + assert_eq!( + actual, + json!({ + "data": "console.log(\"hello deno\");\n", + "hash": "149c777056afcc973d5fcbe11421b6d5ddc57b81786765302030d7fc893bf729" + }) + ); + } + + #[tokio::test] + async fn test_load_tsbuildinfo() { + let mut state = setup( + Some( + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/mod.ts") + .unwrap(), + ), + None, + Some("some content".to_string()), + ) + .await; + let actual = + load(&mut state, json!({ "specifier": "deno:///.tsbuildinfo"})) + .expect("should have invoked op"); + assert_eq!( + actual, + json!({ + "data": "some content", + "hash": null + }) + ); + } + + #[tokio::test] + async fn test_load_missing_specifier() { + let mut state = setup(None, None, None).await; + let actual = load( + &mut state, + json!({ "specifier": "https://deno.land/x/mod.ts"}), + ) + .expect("should have invoked op"); + assert_eq!( + actual, + json!({ + "data": null, + "hash": null, + }) + ) + } + + #[tokio::test] + async fn test_resolve() { + let mut state = setup( + Some( + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts") + .unwrap(), + ), + None, + None, + ) + .await; + let actual = resolve( + &mut state, + json!({ "base": "https://deno.land/x/a.ts", "specifiers": [ "./b.ts" ]}), + ) + .expect("should have invoked op"); + assert_eq!(actual, json!([["https://deno.land/x/b.ts", ".ts"]])); + } + + #[tokio::test] + async fn test_resolve_error() { + let mut state = setup( + Some( + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts") + .unwrap(), + ), + None, + None, + ) + .await; + resolve( + &mut state, + json!({ "base": "https://deno.land/x/a.ts", "specifiers": [ "./bad.ts" ]}), + ).expect_err("should have errored"); + } + + #[tokio::test] + async fn test_respond() { + let mut state = setup(None, None, None).await; + let actual = respond( + &mut state, + json!({ + "diagnostics": [ + { + "messageText": "Unknown compiler option 'invalid'.", + "category": 1, + "code": 5023 + } + ], + "stats": [["a", 12]] + }), + ) + .expect("should have invoked op"); + assert_eq!(actual, json!(true)); + assert_eq!( + state.maybe_response, + Some(RespondArgs { + diagnostics: Diagnostics(vec![Diagnostic { + category: DiagnosticCategory::Error, + code: 5023, + start: None, + end: None, + message_text: Some( + "Unknown compiler option \'invalid\'.".to_string() + ), + message_chain: None, + source: None, + source_line: None, + file_name: None, + related_information: None, + }]), + stats: Stats(vec![("a".to_string(), 12)]) + }) + ); + } + + #[tokio::test] + async fn test_exec() { + let specifier = + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts").unwrap(); + let hash_data = vec![b"something".to_vec()]; + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/tsc2"); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures, + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + builder + .insert(&specifier) + .await + .expect("module not inserted"); + let graph = Rc::new(builder.get_graph(&None).expect("could not get graph")); + let config = TsConfig::new(json!({ + "allowJs": true, + "checkJs": false, + "esModuleInterop": true, + "emitDecoratorMetadata": false, + "incremental": true, + "isolatedModules": true, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + "lib": ["deno.window"], + "module": "esnext", + "noEmit": true, + "outDir": "deno:///", + "strict": true, + "target": "esnext", + "tsBuildInfoFile": "deno:///.tsbuildinfo", + })); + let request = Request { + config, + debug: false, + graph, + hash_data, + maybe_tsbuildinfo: None, + root_names: vec!["https://deno.land/x/a.ts".to_string()], + }; + let actual = exec(js::compiler_isolate_init(), request) + .expect("exec should have not errored"); + assert!(actual.diagnostics.0.is_empty()); + assert!(actual.emitted_files.is_empty()); + assert!(actual.maybe_tsbuildinfo.is_some()); + assert_eq!(actual.stats.0.len(), 12); + } +} |