1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 15:24:46 -05:00

refactor(cli): add tsc2 (#7942)

Ref #7225
This commit is contained in:
Kitson Kelly 2020-10-14 10:52:49 +11:00 committed by GitHub
parent 374d433f1f
commit 10654fa955
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 880 additions and 83 deletions

View file

@ -1,9 +1,12 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
pub fn gen(v: &[&[u8]]) -> String { use ring::digest::Context;
let mut ctx = ring::digest::Context::new(&ring::digest::SHA256); use ring::digest::SHA256;
pub fn gen(v: &[impl AsRef<[u8]>]) -> String {
let mut ctx = Context::new(&SHA256);
for src in v { for src in v {
ctx.update(src); ctx.update(src.as_ref());
} }
let digest = ctx.finish(); let digest = ctx.finish();
let out: Vec<String> = digest let out: Vec<String> = digest
@ -13,3 +16,17 @@ pub fn gen(v: &[&[u8]]) -> String {
.collect(); .collect();
out.join("") out.join("")
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gen() {
let actual = gen(&[b"hello world"]);
assert_eq!(
actual,
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
}

View file

@ -127,7 +127,7 @@ fn format_message(msg: &str, code: &u64) -> String {
} }
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum DiagnosticCategory { pub enum DiagnosticCategory {
Warning, Warning,
Error, Error,
@ -172,7 +172,7 @@ impl From<i64> for DiagnosticCategory {
} }
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DiagnosticMessageChain { pub struct DiagnosticMessageChain {
message_text: String, message_text: String,
@ -199,26 +199,26 @@ impl DiagnosticMessageChain {
} }
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Position { pub struct Position {
pub line: u64, pub line: u64,
pub character: u64, pub character: u64,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Diagnostic { pub struct Diagnostic {
category: DiagnosticCategory, pub category: DiagnosticCategory,
code: u64, pub code: u64,
start: Option<Position>, pub start: Option<Position>,
end: Option<Position>, pub end: Option<Position>,
message_text: Option<String>, pub message_text: Option<String>,
message_chain: Option<DiagnosticMessageChain>, pub message_chain: Option<DiagnosticMessageChain>,
source: Option<String>, pub source: Option<String>,
source_line: Option<String>, pub source_line: Option<String>,
file_name: Option<String>, pub file_name: Option<String>,
related_information: Option<Vec<Diagnostic>>, pub related_information: Option<Vec<Diagnostic>>,
} }
impl 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>); pub struct Diagnostics(pub Vec<Diagnostic>);
impl<'de> Deserialize<'de> for Diagnostics { impl<'de> Deserialize<'de> for Diagnostics {

View file

@ -57,7 +57,7 @@ fn compiler_snapshot() {
.execute( .execute(
"<anon>", "<anon>",
r#" r#"
if (!(bootstrapCompilerRuntime)) { if (!(startup)) {
throw Error("bad"); throw Error("bad");
} }
console.log(`ts version: ${ts.version}`); console.log(`ts version: ${ts.version}`);

View file

@ -51,6 +51,7 @@ mod test_runner;
mod text_encoding; mod text_encoding;
mod tokio_util; mod tokio_util;
mod tsc; mod tsc;
pub mod tsc2;
mod tsc_config; mod tsc_config;
mod upgrade; mod upgrade;
mod version; mod version;

View file

@ -10,7 +10,7 @@ use std::path::PathBuf;
// Update carefully! // Update carefully!
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
#[repr(i32)] #[repr(i32)]
#[derive(Clone, Copy, PartialEq, Debug)] #[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum MediaType { pub enum MediaType {
JavaScript = 0, JavaScript = 0,
JSX = 1, JSX = 1,
@ -19,7 +19,9 @@ pub enum MediaType {
TSX = 4, TSX = 4,
Json = 5, Json = 5,
Wasm = 6, Wasm = 6,
Unknown = 8, TsBuildInfo = 7,
SourceMap = 8,
Unknown = 9,
} }
impl fmt::Display for MediaType { impl fmt::Display for MediaType {
@ -32,6 +34,8 @@ impl fmt::Display for MediaType {
MediaType::TSX => "TSX", MediaType::TSX => "TSX",
MediaType::Json => "Json", MediaType::Json => "Json",
MediaType::Wasm => "Wasm", MediaType::Wasm => "Wasm",
MediaType::TsBuildInfo => "TsBuildInfo",
MediaType::SourceMap => "SourceMap",
MediaType::Unknown => "Unknown", MediaType::Unknown => "Unknown",
}; };
write!(f, "{}", value) 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 { impl MediaType {
fn from_path(path: &Path) -> Self { fn from_path(path: &Path) -> Self {
match path.extension() { 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(os_str) => match os_str.to_str() {
Some("ts") => MediaType::TypeScript, Some("ts") => MediaType::TypeScript,
Some("tsx") => MediaType::TSX, Some("tsx") => MediaType::TSX,
@ -69,10 +85,42 @@ impl MediaType {
Some("cjs") => MediaType::JavaScript, Some("cjs") => MediaType::JavaScript,
Some("json") => MediaType::Json, Some("json") => MediaType::Json,
Some("wasm") => MediaType::Wasm, Some("wasm") => MediaType::Wasm,
Some("tsbuildinfo") => MediaType::TsBuildInfo,
Some("map") => MediaType::SourceMap,
_ => MediaType::Unknown, _ => 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 { impl Serialize for MediaType {
@ -88,7 +136,9 @@ impl Serialize for MediaType {
MediaType::TSX => 4 as i32, MediaType::TSX => 4 as i32,
MediaType::Json => 5 as i32, MediaType::Json => 5 as i32,
MediaType::Wasm => 6 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) Serialize::serialize(&value, serializer)
} }
@ -132,6 +182,14 @@ mod tests {
MediaType::from(Path::new("foo/bar.cjs")), MediaType::from(Path::new("foo/bar.cjs")),
MediaType::JavaScript 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!( assert_eq!(
MediaType::from(Path::new("foo/bar.txt")), MediaType::from(Path::new("foo/bar.txt")),
MediaType::Unknown MediaType::Unknown
@ -148,7 +206,9 @@ mod tests {
assert_eq!(json!(MediaType::TSX), json!(4)); assert_eq!(json!(MediaType::TSX), json!(4));
assert_eq!(json!(MediaType::Json), json!(5)); assert_eq!(json!(MediaType::Json), json!(5));
assert_eq!(json!(MediaType::Wasm), json!(6)); 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] #[test]
@ -160,6 +220,8 @@ mod tests {
assert_eq!(format!("{}", MediaType::TSX), "TSX"); assert_eq!(format!("{}", MediaType::TSX), "TSX");
assert_eq!(format!("{}", MediaType::Json), "Json"); assert_eq!(format!("{}", MediaType::Json), "Json");
assert_eq!(format!("{}", MediaType::Wasm), "Wasm"); assert_eq!(format!("{}", MediaType::Wasm), "Wasm");
assert_eq!(format!("{}", MediaType::TsBuildInfo), "TsBuildInfo");
assert_eq!(format!("{}", MediaType::SourceMap), "SourceMap");
assert_eq!(format!("{}", MediaType::Unknown), "Unknown"); assert_eq!(format!("{}", MediaType::Unknown), "Unknown");
} }
} }

View file

@ -465,7 +465,7 @@ impl ModuleGraphLoader {
filename: source_file.filename.to_str().unwrap().to_string(), filename: source_file.filename.to_str().unwrap().to_string(),
version_hash: checksum::gen(&[ version_hash: checksum::gen(&[
&source_file.source_code.as_bytes(), &source_file.source_code.as_bytes(),
version::DENO.as_bytes(), &version::DENO.as_bytes(),
]), ]),
media_type: source_file.media_type, media_type: source_file.media_type,
source_code: "".to_string(), source_code: "".to_string(),
@ -481,7 +481,7 @@ impl ModuleGraphLoader {
let module_specifier = ModuleSpecifier::from(source_file.url.clone()); let module_specifier = ModuleSpecifier::from(source_file.url.clone());
let version_hash = checksum::gen(&[ let version_hash = checksum::gen(&[
&source_file.source_code.as_bytes(), &source_file.source_code.as_bytes(),
version::DENO.as_bytes(), &version::DENO.as_bytes(),
]); ]);
let source_code = source_file.source_code.clone(); let source_code = source_file.source_code.clone();

View file

@ -373,8 +373,8 @@ impl Module {
} }
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Stats(Vec<(String, u128)>); pub struct Stats(pub Vec<(String, u128)>);
impl<'de> Deserialize<'de> for Stats { impl<'de> Deserialize<'de> for Stats {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
@ -572,6 +572,27 @@ impl Graph2 {
Ok(()) 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 /// Verify the subresource integrity of the graph based upon the optional
/// lockfile, updating the lockfile with any missing resources. This will /// lockfile, updating the lockfile with any missing resources. This will
/// error if any of the resources do not match their lock status. /// error if any of the resources do not match their lock status.
@ -595,6 +616,56 @@ impl Graph2 {
Ok(()) 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 /// Transpile (only transform) the graph, updating any emitted modules
/// with the specifier handler. The result contains any performance stats /// with the specifier handler. The result contains any performance stats
/// from the compiler and optionally any user provided configuration compiler /// from the compiler and optionally any user provided configuration compiler
@ -798,7 +869,7 @@ impl GraphBuilder2 {
} }
#[cfg(test)] #[cfg(test)]
mod tests { pub mod tests {
use super::*; use super::*;
use deno_core::futures::future; use deno_core::futures::future;

View file

@ -0,0 +1 @@
console.log("hello deno");

View file

@ -0,0 +1,3 @@
import * as b from "./b.ts";
console.log(b);

View file

@ -0,0 +1 @@
export const b = "b";

View file

@ -0,0 +1 @@
console.log("hello deno");

View file

@ -417,7 +417,7 @@ impl TsCompiler {
{ {
let existing_hash = crate::checksum::gen(&[ let existing_hash = crate::checksum::gen(&[
&source_file.source_code.as_bytes(), &source_file.source_code.as_bytes(),
version::DENO.as_bytes(), &version::DENO.as_bytes(),
]); ]);
let expected_hash = let expected_hash =
file_info["version"].as_str().unwrap().to_string(); file_info["version"].as_str().unwrap().to_string();
@ -988,7 +988,7 @@ fn execute_in_tsc(
} }
let bootstrap_script = format!( let bootstrap_script = format!(
"globalThis.bootstrapCompilerRuntime({{ debugFlag: {} }})", "globalThis.startup({{ debugFlag: {}, legacy: true }})",
debug_flag debug_flag
); );
js_runtime.execute("<compiler>", &bootstrap_script)?; js_runtime.execute("<compiler>", &bootstrap_script)?;

View file

@ -4,7 +4,7 @@
// that is created when Deno needs to compile TS/WASM to JS. // that is created when Deno needs to compile TS/WASM to JS.
// //
// It provides two functions that should be called by Rust: // It provides two functions that should be called by Rust:
// - `bootstrapCompilerRuntime` // - `startup`
// This functions must be called when creating isolate // This functions must be called when creating isolate
// to properly setup runtime. // to properly setup runtime.
// - `tsCompilerOnMessage` // - `tsCompilerOnMessage`
@ -54,6 +54,9 @@ delete Object.prototype.__proto__;
} }
} }
/** @type {Map<string, ts.SourceFile>} */
const sourceFileCache = new Map();
/** /**
* @param {import("../dts/typescript").DiagnosticRelatedInformation} diagnostic * @param {import("../dts/typescript").DiagnosticRelatedInformation} diagnostic
*/ */
@ -296,15 +299,15 @@ delete Object.prototype.__proto__;
debug(`host.fileExists("${fileName}")`); debug(`host.fileExists("${fileName}")`);
return false; return false;
}, },
readFile(fileName) { readFile(specifier) {
debug(`host.readFile("${fileName}")`); debug(`host.readFile("${specifier}")`);
if (legacy) { if (legacy) {
if (fileName == TS_BUILD_INFO) { if (specifier == TS_BUILD_INFO) {
return legacyHostState.buildInfo; return legacyHostState.buildInfo;
} }
return unreachable(); return unreachable();
} else { } else {
return core.jsonOpSync("op_read_file", { fileName }).data; return core.jsonOpSync("op_load", { specifier }).data;
} }
}, },
getSourceFile( getSourceFile(
@ -338,6 +341,14 @@ delete Object.prototype.__proto__;
); );
sourceFile.tsSourceFile.version = sourceFile.versionHash; sourceFile.tsSourceFile.version = sourceFile.versionHash;
delete sourceFile.sourceCode; 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; return sourceFile.tsSourceFile;
} catch (e) { } catch (e) {
@ -349,37 +360,26 @@ delete Object.prototype.__proto__;
return undefined; return undefined;
} }
} else { } else {
const sourceFile = sourceFileCache.get(specifier); let sourceFile = sourceFileCache.get(specifier);
if (sourceFile) { if (sourceFile) {
return sourceFile; return sourceFile;
} }
try { /** @type {{ data: string; hash: string; }} */
/** @type {{ data: string; hash: string; }} */ const { data, hash } = core.jsonOpSync(
const { data, hash } = core.jsonOpSync( "op_load",
"op_load_module", { specifier },
{ specifier }, );
); assert(data, `"data" is unexpectedly null for "${specifier}".`);
const sourceFile = ts.createSourceFile( sourceFile = ts.createSourceFile(
specifier, specifier,
data, data,
languageVersion, languageVersion,
); );
sourceFile.moduleName = specifier; sourceFile.moduleName = specifier;
sourceFile.version = hash; sourceFile.version = hash;
sourceFileCache.set(specifier, sourceFile); sourceFileCache.set(specifier, sourceFile);
return 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;
}
}
} }
}, },
getDefaultLibFileName() { getDefaultLibFileName() {
@ -392,7 +392,7 @@ delete Object.prototype.__proto__;
return `${ASSETS}/lib.deno.worker.d.ts`; return `${ASSETS}/lib.deno.worker.d.ts`;
} }
} else { } else {
return `lib.esnext.d.ts`; return `${ASSETS}/lib.esnext.d.ts`;
} }
}, },
getDefaultLibLocation() { getDefaultLibLocation() {
@ -403,16 +403,14 @@ delete Object.prototype.__proto__;
if (legacy) { if (legacy) {
legacyHostState.writeFile(fileName, data, sourceFiles); legacyHostState.writeFile(fileName, data, sourceFiles);
} else { } else {
let maybeModuleName; let maybeSpecifiers;
if (sourceFiles) { if (sourceFiles) {
assert(sourceFiles.length === 1, "unexpected number of source files"); maybeSpecifiers = sourceFiles.map((sf) => sf.moduleName);
const [sourceFile] = sourceFiles; debug(` specifiers: ${maybeSpecifiers.join(", ")}`);
maybeModuleName = sourceFile.moduleName;
debug(` moduleName: ${maybeModuleName}`);
} }
return core.jsonOpSync( return core.jsonOpSync(
"op_write_file", "op_emit",
{ maybeModuleName, fileName, data }, { maybeSpecifiers, fileName, data },
); );
} }
}, },
@ -463,7 +461,7 @@ delete Object.prototype.__proto__;
return resolved; return resolved;
} else { } else {
/** @type {Array<[string, import("../dts/typescript").Extension]>} */ /** @type {Array<[string, import("../dts/typescript").Extension]>} */
const resolved = core.jsonOpSync("op_resolve_specifiers", { const resolved = core.jsonOpSync("op_resolve", {
specifiers, specifiers,
base, base,
}); });
@ -737,6 +735,7 @@ delete Object.prototype.__proto__;
1208, 1208,
]; ];
/** @type {Array<{ key: string, value: number }>} */
const stats = []; const stats = [];
let statsStart = 0; let statsStart = 0;
@ -779,7 +778,6 @@ delete Object.prototype.__proto__;
} }
function performanceEnd() { function performanceEnd() {
// TODO(kitsonk) replace with performance.measure() when landed
const duration = new Date() - statsStart; const duration = new Date() - statsStart;
stats.push({ key: "Compile time", value: duration }); stats.push({ key: "Compile time", value: duration });
return stats; 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 }) { /** The API that is called by Rust when executing a request.
if (hasBootstrapped) { * @param {Request} request
throw new Error("Worker runtime already bootstrapped"); */
} function exec({ config, debug: debugFlag, rootNames }) {
hasBootstrapped = true; setLogDebug(debugFlag, "TS");
delete globalThis.__bootstrap; performanceStart();
core.ops(); debug(">>> exec start", { rootNames });
setLogDebug(!!debugFlag, "TS"); 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");
} }
globalThis.bootstrapCompilerRuntime = bootstrapCompilerRuntime; 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.");
}
hasStarted = true;
core.ops();
core.registerErrorClass("Error", Error);
setLogDebug(!!debugFlag, "TS");
legacy = legacyFlag;
}
globalThis.startup = startup;
globalThis.exec = exec;
// TODO(@kitsonk) remove when converted from legacy tsc
globalThis.tsCompilerOnMessage = tsCompilerOnMessage; globalThis.tsCompilerOnMessage = tsCompilerOnMessage;
})(this); })(this);

584
cli/tsc2.rs Normal file
View file

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

View file

@ -1,5 +1,8 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
pub use anyhow::anyhow;
pub use anyhow::bail;
pub use anyhow::Context;
use rusty_v8 as v8; use rusty_v8 as v8;
use std::borrow::Cow; use std::borrow::Cow;
use std::convert::TryFrom; use std::convert::TryFrom;