From d2f96a32613dbb3d47f7ab6e9c4ada012edb6289 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Thu, 17 Dec 2020 16:41:01 +1100 Subject: [PATCH] refactor(cli): runtime compiler APIs consolidated to Deno.emit() Closes: #4752 --- cli/diagnostics.rs | 4 +- cli/dts/lib.deno.unstable.d.ts | 185 +++++++--------- cli/module_graph.rs | 226 +++++++++++++------- cli/ops/runtime_compiler.rs | 155 +++++--------- cli/tests/compiler_api_test.ts | 319 +++++++++++++++++----------- cli/tests/lib_ref.ts | 20 +- cli/tests/lib_ref.ts.out | 2 +- cli/tests/lib_runtime_api.ts | 16 +- cli/tests/lib_runtime_api.ts.out | 2 +- cli/tests/subdir/worker_unstable.ts | 2 +- cli/tests/unstable_worker.ts.out | 2 +- cli/tsc_config.rs | 17 ++ runtime/rt/40_compiler_api.js | 139 ++++++------ runtime/rt/90_deno_ns.js | 4 +- 14 files changed, 589 insertions(+), 504 deletions(-) diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs index 419d89a97c9f20..773ee0b36d8035 100644 --- a/cli/diagnostics.rs +++ b/cli/diagnostics.rs @@ -44,11 +44,10 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[ "UnixListenOptions", "WritePermissionDescriptor", "applySourceMap", - "bundle", - "compile", "connect", "consoleSize", "createHttpClient", + "emit", "formatDiagnostics", "futime", "futimeSync", @@ -77,7 +76,6 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[ "symlinkSync", "systemMemoryInfo", "systemCpuInfo", - "transpileOnly", "umask", "utime", "utimeSync", diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index f9ef1fb2fcb88c..bad2ddbc097bee 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -290,6 +290,8 @@ declare namespace Deno { /** Base directory to resolve non-relative module names. Defaults to * `undefined`. */ baseUrl?: string; + /** The character set of the input files. Defaults to `"utf8"`. */ + charset?: string; /** Report errors in `.js` files. Use in conjunction with `allowJs`. Defaults * to `false`. */ checkJs?: boolean; @@ -393,12 +395,17 @@ declare namespace Deno { /** Do not emit `"use strict"` directives in module output. Defaults to * `false`. */ noImplicitUseStrict?: boolean; + /** Do not include the default library file (`lib.d.ts`). Defaults to + * `false`. */ + noLib?: boolean; /** Do not add triple-slash references or module import targets to the list of * compiled files. Defaults to `false`. */ noResolve?: boolean; /** Disable strict checking of generic signatures in function types. Defaults * to `false`. */ noStrictGenericChecks?: boolean; + /** Include 'undefined' in index signature results. Defaults to `false`. */ + noUncheckedIndexedAccess?: boolean; /** Report errors on unused locals. Defaults to `false`. */ noUnusedLocals?: boolean; /** Report errors on unused parameters. Defaults to `false`. */ @@ -487,122 +494,78 @@ declare namespace Deno { useDefineForClassFields?: boolean; } - /** **UNSTABLE**: new API, yet to be vetted. - * - * The results of a transpile only command, where the `source` contains the - * emitted source, and `map` optionally contains the source map. */ - export interface TranspileOnlyResult { - source: string; - map?: string; + interface ImportMap { + imports: Record; + scopes?: Record>; } - /** **UNSTABLE**: new API, yet to be vetted. - * - * Takes a set of TypeScript sources and resolves to a map where the key was - * the original file name provided in sources and the result contains the - * `source` and optionally the `map` from the transpile operation. This does no - * type checking and validation, it effectively "strips" the types from the - * file. - * - * ```ts - * const results = await Deno.transpileOnly({ - * "foo.ts": `const foo: string = "foo";` - * }); - * ``` - * - * @param sources A map where the key is the filename and the value is the text - * to transpile. The filename is only used in the transpile and - * not resolved, for example to fill in the source name in the - * source map. - * @param options An option object of options to send to the compiler. This is - * a subset of ts.CompilerOptions which can be supported by Deno. - * If unsupported option is passed then the API will throw an error. - */ - export function transpileOnly( - sources: Record, - options?: CompilerOptions, - ): Promise>; + interface EmitOptions { + /** Indicate that the source code should be emitted to a single file + * JavaScript bundle that is an ES module (`"esm"`). */ + bundle?: "esm"; + /** If `true` then the sources will be typed checked, returning any + * diagnostic errors in the result. If `false` type checking will be + * skipped. Defaults to `true`. + * + * *Note* by default, only TypeScript will be type checked, just like on + * the command line. Use the `compilerOptions` options of `checkJs` to + * enable type checking of JavaScript. */ + check?: boolean; + /** A set of options that are aligned to TypeScript compiler options that + * are supported by Deno. */ + compilerOptions?: CompilerOptions; + /** An [import-map](https://deno.land/manual/linking_to_external_code/import_maps#import-maps) + * which will be applied to the imports. */ + importMap?: ImportMap; + /** An absolute path to an [import-map](https://deno.land/manual/linking_to_external_code/import_maps#import-maps). + * Required to be specified if an `importMap` is specified to be able to + * determine resolution of relative paths. If a `importMap` is not + * specified, then it will assumed the file path points to an import map on + * disk and will be attempted to be loaded based on current runtime + * permissions. + */ + importMapPath?: string; + /** A record of sources to use when doing the emit. If provided, Deno will + * use these sources instead of trying to resolve the modules externally. */ + sources?: Record; + } - /** **UNSTABLE**: new API, yet to be vetted. - * - * Takes a root module name, and optionally a record set of sources. Resolves - * with a compiled set of modules and possibly diagnostics if the compiler - * encountered any issues. If just a root name is provided, the modules - * will be resolved as if the root module had been passed on the command line. - * - * If sources are passed, all modules will be resolved out of this object, where - * the key is the module name and the value is the content. The extension of - * the module name will be used to determine the media type of the module. - * - * ```ts - * const [ maybeDiagnostics1, output1 ] = await Deno.compile("foo.ts"); - * - * const [ maybeDiagnostics2, output2 ] = await Deno.compile("/foo.ts", { - * "/foo.ts": `export * from "./bar.ts";`, - * "/bar.ts": `export const bar = "bar";` - * }); - * ``` - * - * @param rootName The root name of the module which will be used as the - * "starting point". If no `sources` is specified, Deno will - * resolve the module externally as if the `rootName` had been - * specified on the command line. - * @param sources An optional key/value map of sources to be used when resolving - * modules, where the key is the module name, and the value is - * the source content. The extension of the key will determine - * the media type of the file when processing. If supplied, - * Deno will not attempt to resolve any modules externally. - * @param options An optional object of options to send to the compiler. This is - * a subset of ts.CompilerOptions which can be supported by Deno. - */ - export function compile( - rootName: string, - sources?: Record, - options?: CompilerOptions, - ): Promise<[Diagnostic[] | undefined, Record]>; + interface EmitResult { + /** Diagnostic messages returned from the type checker (`tsc`). */ + diagnostics: Diagnostic[]; + /** Any emitted files. If bundled, then the JavaScript will have the + * key of `deno:///bundle.js` with an optional map (based on + * `compilerOptions`) in `deno:///bundle.js.map`. */ + files: Record; + /** An optional array of any compiler options that were ignored by Deno. */ + ignoredOptions?: string[]; + /** An array of internal statics related to the emit, for diagnostic + * purposes. */ + stats: Array<[string, number]>; + } - /** **UNSTABLE**: new API, yet to be vetted. - * - * `bundle()` is part the compiler API. A full description of this functionality - * can be found in the [manual](https://deno.land/manual/runtime/compiler_apis#denobundle). - * - * Takes a root module name, and optionally a record set of sources. Resolves - * with a single JavaScript string (and bundle diagnostics if issues arise with - * the bundling) that is like the output of a `deno bundle` command. If just - * a root name is provided, the modules will be resolved as if the root module - * had been passed on the command line. - * - * If sources are passed, all modules will be resolved out of this object, where - * the key is the module name and the value is the content. The extension of the - * module name will be used to determine the media type of the module. - * - * ```ts - * // equivalent to "deno bundle foo.ts" from the command line - * const [ maybeDiagnostics1, output1 ] = await Deno.bundle("foo.ts"); - * - * const [ maybeDiagnostics2, output2 ] = await Deno.bundle("/foo.ts", { - * "/foo.ts": `export * from "./bar.ts";`, - * "/bar.ts": `export const bar = "bar";` - * }); - * ``` - * - * @param rootName The root name of the module which will be used as the - * "starting point". If no `sources` is specified, Deno will - * resolve the module externally as if the `rootName` had been - * specified on the command line. - * @param sources An optional key/value map of sources to be used when resolving - * modules, where the key is the module name, and the value is - * the source content. The extension of the key will determine - * the media type of the file when processing. If supplied, - * Deno will not attempt to resolve any modules externally. - * @param options An optional object of options to send to the compiler. This is - * a subset of ts.CompilerOptions which can be supported by Deno. + /** + * **UNSTABLE**: new API, yet to be vetted. + * + * Similar to the command line functionality of `deno run` or `deno cache`, + * `Deno.emit()` provides a way to provide Deno arbitrary JavaScript + * or TypeScript and have it return JavaScript based on the options and + * settings provided. The source code can either be provided or the modules + * can be fetched and resolved like other modules are resolved. + * + * Requires `allow-read` and/or `allow-net` if sources are not provided. + * + * @param rootSpecifier The specifier that will be used as the entry point. + * If no sources are provided, then the specifier would + * be the same as if you typed it on the command line for + * `deno run`. If sources are provided, it should match + * one of the names of the sources. + * @param options A set of options to be used with the emit. */ - export function bundle( - rootName: string, - sources?: Record, - options?: CompilerOptions, - ): Promise<[Diagnostic[] | undefined, string]>; + export function emit( + rootSpecifier: string | URL, + options?: EmitOptions, + ): Promise; /** **UNSTABLE**: Should not have same name as `window.location` type. */ interface Location { diff --git a/cli/module_graph.rs b/cli/module_graph.rs index f5e08882ed5209..a8d2ac72e15dd8 100644 --- a/cli/module_graph.rs +++ b/cli/module_graph.rs @@ -501,18 +501,27 @@ impl Module { } #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct Stats(pub Vec<(String, u128)>); +pub struct Stats(pub Vec<(String, u32)>); impl<'de> Deserialize<'de> for Stats { fn deserialize(deserializer: D) -> result::Result where D: Deserializer<'de>, { - let items: Vec<(String, u128)> = Deserialize::deserialize(deserializer)?; + let items: Vec<(String, u32)> = Deserialize::deserialize(deserializer)?; Ok(Stats(items)) } } +impl Serialize for Stats { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Serialize::serialize(&self.0, serializer) + } +} + impl fmt::Display for Stats { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Compilation statistics:")?; @@ -624,6 +633,10 @@ impl Default for BundleType { #[derive(Debug, Default)] pub struct EmitOptions { + /// If true, then code will be type checked, otherwise type checking will be + /// skipped. If false, then swc will be used for the emit, otherwise tsc will + /// be used. + pub check: bool, /// Indicate the form the result of the emit should take. pub bundle_type: BundleType, /// If `true` then debug logging will be output from the isolate. @@ -773,8 +786,8 @@ impl Graph { let s = self.emit_bundle(&root_specifier, &ts_config.into())?; let stats = Stats(vec![ - ("Files".to_string(), self.modules.len() as u128), - ("Total time".to_string(), start.elapsed().as_millis()), + ("Files".to_string(), self.modules.len() as u32), + ("Total time".to_string(), start.elapsed().as_millis() as u32), ]); Ok((s, stats, maybe_ignored_options)) @@ -922,18 +935,22 @@ impl Graph { /// emitting single modules as well as bundles, using Deno module resolution /// or supplied sources. pub fn emit( - self, + mut self, options: EmitOptions, ) -> Result<(HashMap, ResultInfo), AnyError> { let mut config = TsConfig::new(json!({ "allowJs": true, + "checkJs": false, // TODO(@kitsonk) consider enabling this by default // see: https://github.com/denoland/deno/issues/7732 "emitDecoratorMetadata": false, "esModuleInterop": true, "experimentalDecorators": true, + "inlineSourceMap": false, "isolatedModules": true, "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", "lib": TypeLib::DenoWindow, "module": "esnext", "strict": true, @@ -941,11 +958,7 @@ impl Graph { })); let opts = match options.bundle_type { BundleType::Esm => json!({ - "checkJs": false, - "inlineSourceMap": false, "noEmit": true, - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", }), BundleType::None => json!({ "outDir": "deno://", @@ -961,74 +974,141 @@ impl Graph { None }; - let root_names = self.get_root_names(!config.get_check_js()); - let hash_data = - vec![config.as_bytes(), version::deno().as_bytes().to_owned()]; - let graph = Rc::new(RefCell::new(self)); - - let response = tsc::exec( - js::compiler_isolate_init(), - tsc::Request { - config: config.clone(), - debug: options.debug, - graph: graph.clone(), - hash_data, - maybe_tsbuildinfo: None, - root_names, - }, - )?; + if !options.check && config.get_declaration() { + return Err(anyhow!("The option of `check` is false, but the compiler option of `declaration` is true which is not currently supported.")); + } + if options.bundle_type != BundleType::None && config.get_declaration() { + return Err(anyhow!("The bundle option is set, but the compiler option of `declaration` is true which is not currently supported.")); + } let mut emitted_files = HashMap::new(); - let graph = graph.borrow(); - match options.bundle_type { - BundleType::Esm => { - assert!( - response.emitted_files.is_empty(), - "No files should have been emitted from tsc." - ); - assert_eq!( - graph.roots.len(), - 1, - "Only a single root module supported." - ); - let specifier = &graph.roots[0]; - let s = graph.emit_bundle(specifier, &config.into())?; - emitted_files.insert("deno:///bundle.js".to_string(), s); - } - BundleType::None => { - for emitted_file in &response.emitted_files { + if options.check { + let root_names = self.get_root_names(!config.get_check_js()); + let hash_data = + vec![config.as_bytes(), version::deno().as_bytes().to_owned()]; + let graph = Rc::new(RefCell::new(self)); + let response = tsc::exec( + js::compiler_isolate_init(), + tsc::Request { + config: config.clone(), + debug: options.debug, + graph: graph.clone(), + hash_data, + maybe_tsbuildinfo: None, + root_names, + }, + )?; + + let graph = graph.borrow(); + match options.bundle_type { + BundleType::Esm => { assert!( - emitted_file.maybe_specifiers.is_some(), - "Orphaned file emitted." + response.emitted_files.is_empty(), + "No files should have been emitted from tsc." ); - let specifiers = emitted_file.maybe_specifiers.clone().unwrap(); assert_eq!( - specifiers.len(), + graph.roots.len(), 1, - "An unexpected number of specifiers associated with emitted file." + "Only a single root module supported." ); - let specifier = specifiers[0].clone(); - let extension = match emitted_file.media_type { - MediaType::JavaScript => ".js", - MediaType::SourceMap => ".js.map", - MediaType::Dts => ".d.ts", - _ => unreachable!(), - }; - let key = format!("{}{}", specifier, extension); - emitted_files.insert(key, emitted_file.data.clone()); + let specifier = &graph.roots[0]; + let s = graph.emit_bundle(specifier, &config.into())?; + emitted_files.insert("deno:///bundle.js".to_string(), s); + } + BundleType::None => { + for emitted_file in &response.emitted_files { + assert!( + emitted_file.maybe_specifiers.is_some(), + "Orphaned file emitted." + ); + let specifiers = emitted_file.maybe_specifiers.clone().unwrap(); + assert_eq!( + specifiers.len(), + 1, + "An unexpected number of specifiers associated with emitted file." + ); + let specifier = specifiers[0].clone(); + let extension = match emitted_file.media_type { + MediaType::JavaScript => ".js", + MediaType::SourceMap => ".js.map", + MediaType::Dts => ".d.ts", + _ => unreachable!(), + }; + let key = format!("{}{}", specifier, extension); + emitted_files.insert(key, emitted_file.data.clone()); + } + } + }; + + Ok(( + emitted_files, + ResultInfo { + diagnostics: response.diagnostics, + loadable_modules: graph.get_loadable_modules(), + maybe_ignored_options, + stats: response.stats, + }, + )) + } else { + let start = Instant::now(); + let mut emit_count = 0_u32; + match options.bundle_type { + BundleType::Esm => { + assert_eq!( + self.roots.len(), + 1, + "Only a single root module supported." + ); + let specifier = &self.roots[0]; + let s = self.emit_bundle(specifier, &config.into())?; + emit_count += 1; + emitted_files.insert("deno:///bundle.js".to_string(), s); + } + BundleType::None => { + let emit_options: ast::EmitOptions = config.into(); + for (_, module_slot) in self.modules.iter_mut() { + if let ModuleSlot::Module(module) = module_slot { + if !(emit_options.check_js + || module.media_type == MediaType::JSX + || module.media_type == MediaType::TSX + || module.media_type == MediaType::TypeScript) + { + emitted_files + .insert(module.specifier.to_string(), module.source.clone()); + } + if module.maybe_parsed_module.is_none() { + module.parse()?; + } + let parsed_module = module.maybe_parsed_module.clone().unwrap(); + let (code, maybe_map) = parsed_module.transpile(&emit_options)?; + emit_count += 1; + emitted_files.insert(format!("{}.js", module.specifier), code); + if let Some(map) = maybe_map { + emitted_files + .insert(format!("{}.js.map", module.specifier), map); + } + } + } + self.flush()?; } } - }; - Ok(( - emitted_files, - ResultInfo { - diagnostics: response.diagnostics, - loadable_modules: graph.get_loadable_modules(), - maybe_ignored_options, - stats: response.stats, - }, - )) + let stats = Stats(vec![ + ("Files".to_string(), self.modules.len() as u32), + ("Emitted".to_string(), emit_count), + ("Total time".to_string(), start.elapsed().as_millis() as u32), + ]); + + Ok(( + emitted_files, + ResultInfo { + diagnostics: Default::default(), + loadable_modules: self.get_loadable_modules(), + maybe_ignored_options, + stats, + }, + )) + } } /// Shared between `bundle()` and `emit()`. @@ -1568,10 +1648,9 @@ impl Graph { let maybe_ignored_options = ts_config.merge_tsconfig(options.maybe_config_path)?; - let emit_options: ast::EmitOptions = ts_config.clone().into(); - - let mut emit_count: u128 = 0; let config = ts_config.as_bytes(); + let emit_options: ast::EmitOptions = ts_config.into(); + let mut emit_count = 0_u32; for (_, module_slot) in self.modules.iter_mut() { if let ModuleSlot::Module(module) = module_slot { // TODO(kitsonk) a lot of this logic should be refactored into `Module` as @@ -1609,9 +1688,9 @@ impl Graph { self.flush()?; let stats = Stats(vec![ - ("Files".to_string(), self.modules.len() as u128), + ("Files".to_string(), self.modules.len() as u32), ("Emitted".to_string(), emit_count), - ("Total time".to_string(), start.elapsed().as_millis()), + ("Total time".to_string(), start.elapsed().as_millis() as u32), ]); Ok(ResultInfo { @@ -2277,6 +2356,7 @@ pub mod tests { .await; let (emitted_files, result_info) = graph .emit(EmitOptions { + check: true, bundle_type: BundleType::None, debug: false, maybe_user_config: None, @@ -2317,6 +2397,7 @@ pub mod tests { .await; let (emitted_files, result_info) = graph .emit(EmitOptions { + check: true, bundle_type: BundleType::Esm, debug: false, maybe_user_config: None, @@ -2354,6 +2435,7 @@ pub mod tests { user_config.insert("declaration".to_string(), json!(true)); let (emitted_files, result_info) = graph .emit(EmitOptions { + check: true, bundle_type: BundleType::None, debug: false, maybe_user_config: Some(user_config), diff --git a/cli/ops/runtime_compiler.rs b/cli/ops/runtime_compiler.rs index ec9806e60c7175..47b6301eafba91 100644 --- a/cli/ops/runtime_compiler.rs +++ b/cli/ops/runtime_compiler.rs @@ -1,8 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -use crate::ast; -use crate::colors; -use crate::media_type::MediaType; +use crate::import_map::ImportMap; use crate::module_graph::BundleType; use crate::module_graph::EmitOptions; use crate::module_graph::GraphBuilder; @@ -10,16 +8,16 @@ use crate::program_state::ProgramState; use crate::specifier_handler::FetchHandler; use crate::specifier_handler::MemoryHandler; use crate::specifier_handler::SpecifierHandler; -use crate::tsc_config; use deno_runtime::permissions::Permissions; use std::sync::Arc; +use deno_core::error::anyhow; +use deno_core::error::generic_error; use deno_core::error::AnyError; -use deno_core::error::Context; -use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; +use deno_core::url::Url; use deno_core::BufVec; use deno_core::ModuleSpecifier; use deno_core::OpState; @@ -29,37 +27,46 @@ use std::collections::HashMap; use std::rc::Rc; pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_async(rt, "op_compile", op_compile); - super::reg_json_async(rt, "op_transpile", op_transpile); + super::reg_json_async(rt, "op_emit", op_emit); } -#[derive(Deserialize, Debug)] +#[derive(Debug, Deserialize)] +enum RuntimeBundleType { + #[serde(rename = "esm")] + Esm, +} + +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -struct CompileArgs { - root_name: String, +struct EmitArgs { + bundle: Option, + check: Option, + compiler_options: Option>, + import_map: Option, + import_map_path: Option, + root_specifier: String, sources: Option>, - bundle: bool, - options: Option, } -async fn op_compile( +async fn op_emit( state: Rc>, args: Value, _data: BufVec, ) -> Result { - let args: CompileArgs = serde_json::from_value(args)?; - if args.bundle { - deno_runtime::ops::check_unstable2(&state, "Deno.bundle"); - } else { - deno_runtime::ops::check_unstable2(&state, "Deno.compile"); - } + deno_runtime::ops::check_unstable2(&state, "Deno.emit"); + let args: EmitArgs = serde_json::from_value(args)?; let program_state = state.borrow().borrow::>().clone(); let runtime_permissions = { let state = state.borrow(); state.borrow::().clone() }; + // when we are actually resolving modules without provided sources, we should + // treat the root module as a dynamic import so that runtime permissions are + // applied. + let mut is_dynamic = false; let handler: Rc> = if let Some(sources) = args.sources { + is_dynamic = true; Rc::new(RefCell::new(MemoryHandler::new(sources))) } else { Rc::new(RefCell::new(FetchHandler::new( @@ -67,93 +74,43 @@ async fn op_compile( runtime_permissions, )?)) }; - let mut builder = GraphBuilder::new(handler, None, None); - let specifier = ModuleSpecifier::resolve_url_or_path(&args.root_name) - .context("The root specifier is invalid.")?; - builder.add(&specifier, false).await?; - let graph = builder.get_graph(); - let bundle_type = if args.bundle { - BundleType::Esm + let maybe_import_map = if let Some(import_map_str) = args.import_map_path { + let import_map_url = + Url::from_file_path(&import_map_str).map_err(|_| { + anyhow!("Bad file path (\"{}\") for import map.", import_map_str) + })?; + let import_map = if let Some(value) = args.import_map { + ImportMap::from_json(&import_map_url.to_string(), &value.to_string())? + } else { + ImportMap::load(&import_map_str)? + }; + Some(import_map) + } else if args.import_map.is_some() { + return Err(generic_error("An importMap was specified, but no importMapPath was provided, which is required.")); } else { - BundleType::None + None + }; + let mut builder = GraphBuilder::new(handler, maybe_import_map, None); + let root_specifier = + ModuleSpecifier::resolve_url_or_path(&args.root_specifier)?; + builder.add(&root_specifier, is_dynamic).await?; + let bundle_type = match args.bundle { + Some(RuntimeBundleType::Esm) => BundleType::Esm, + _ => BundleType::None, }; + let graph = builder.get_graph(); let debug = program_state.flags.log_level == Some(log::Level::Debug); - let maybe_user_config: Option> = - if let Some(options) = args.options { - Some(serde_json::from_str(&options)?) - } else { - None - }; - let (emitted_files, result_info) = graph.emit(EmitOptions { + let (files, result_info) = graph.emit(EmitOptions { bundle_type, + check: args.check.unwrap_or(true), debug, - maybe_user_config, + maybe_user_config: args.compiler_options, })?; Ok(json!({ - "emittedFiles": emitted_files, "diagnostics": result_info.diagnostics, + "files": files, + "ignoredOptions": result_info.maybe_ignored_options, + "stats": result_info.stats, })) } - -#[derive(Deserialize, Debug)] -struct TranspileArgs { - sources: HashMap, - options: Option, -} - -#[derive(Debug, Serialize)] -struct RuntimeTranspileEmit { - source: String, - map: Option, -} - -async fn op_transpile( - state: Rc>, - args: Value, - _data: BufVec, -) -> Result { - deno_runtime::ops::check_unstable2(&state, "Deno.transpileOnly"); - let args: TranspileArgs = serde_json::from_value(args)?; - - let mut compiler_options = tsc_config::TsConfig::new(json!({ - "checkJs": true, - "emitDecoratorMetadata": false, - "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", - "inlineSourceMap": false, - })); - - let user_options: HashMap = if let Some(options) = args.options - { - serde_json::from_str(&options)? - } else { - HashMap::new() - }; - let maybe_ignored_options = - compiler_options.merge_user_config(&user_options)?; - // TODO(@kitsonk) these really should just be passed back to the caller - if let Some(ignored_options) = maybe_ignored_options { - info!("{}: {}", colors::yellow("warning"), ignored_options); - } - - let emit_options: ast::EmitOptions = compiler_options.into(); - let mut emit_map = HashMap::new(); - - for (specifier, source) in args.sources { - let media_type = MediaType::from(&specifier); - let parsed_module = ast::parse(&specifier, &source, &media_type)?; - let (source, maybe_source_map) = parsed_module.transpile(&emit_options)?; - - emit_map.insert( - specifier.to_string(), - RuntimeTranspileEmit { - source, - map: maybe_source_map, - }, - ); - } - let result = serde_json::to_value(emit_map)?; - Ok(result) -} diff --git a/cli/tests/compiler_api_test.ts b/cli/tests/compiler_api_test.ts index 4535ad6edd3f09..ead3e704e3f821 100644 --- a/cli/tests/compiler_api_test.ts +++ b/cli/tests/compiler_api_test.ts @@ -6,15 +6,21 @@ import { } from "../../std/testing/asserts.ts"; Deno.test({ - name: "Deno.compile() - sources provided", + name: "Deno.emit() - sources provided", async fn() { - const [diagnostics, actual] = await Deno.compile("/foo.ts", { - "/foo.ts": `import * as bar from "./bar.ts";\n\nconsole.log(bar);\n`, - "/bar.ts": `export const bar = "bar";\n`, - }); - assert(diagnostics == null); - assert(actual); - const keys = Object.keys(actual).sort(); + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "/foo.ts", + { + sources: { + "/foo.ts": `import * as bar from "./bar.ts";\n\nconsole.log(bar);\n`, + "/bar.ts": `export const bar = "bar";\n`, + }, + }, + ); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + const keys = Object.keys(files).sort(); assert(keys[0].endsWith("/bar.ts.js")); assert(keys[1].endsWith("/bar.ts.js.map")); assert(keys[2].endsWith("/foo.ts.js")); @@ -23,12 +29,15 @@ Deno.test({ }); Deno.test({ - name: "Deno.compile() - no sources provided", + name: "Deno.emit() - no sources provided", async fn() { - const [diagnostics, actual] = await Deno.compile("./subdir/mod1.ts"); - assert(diagnostics == null); - assert(actual); - const keys = Object.keys(actual).sort(); + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "./subdir/mod1.ts", + ); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + const keys = Object.keys(files).sort(); assertEquals(keys.length, 6); assert(keys[0].endsWith("cli/tests/subdir/mod1.ts.js")); assert(keys[1].endsWith("cli/tests/subdir/mod1.ts.js.map")); @@ -36,183 +45,249 @@ Deno.test({ }); Deno.test({ - name: "Deno.compile() - compiler options effects emit", + name: "Deno.emit() - compiler options effects emit", async fn() { - const [diagnostics, actual] = await Deno.compile( + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( "/foo.ts", { - "/foo.ts": `export const foo = "foo";`, - }, - { - module: "amd", - sourceMap: false, + compilerOptions: { + module: "amd", + sourceMap: false, + }, + sources: { "/foo.ts": `export const foo = "foo";` }, }, ); - assert(diagnostics == null); - assert(actual); - const keys = Object.keys(actual); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + const keys = Object.keys(files); assertEquals(keys.length, 1); const key = keys[0]; assert(key.endsWith("/foo.ts.js")); - assert(actual[key].startsWith("define(")); + assert(files[key].startsWith("define(")); }, }); Deno.test({ - name: "Deno.compile() - pass lib in compiler options", + name: "Deno.emit() - pass lib in compiler options", async fn() { - const [diagnostics, actual] = await Deno.compile( + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( "file:///foo.ts", { - "file:///foo.ts": `console.log(document.getElementById("foo")); - console.log(Deno.args);`, + compilerOptions: { + lib: ["dom", "es2018", "deno.ns"], + }, + sources: { + "file:///foo.ts": `console.log(document.getElementById("foo")); + console.log(Deno.args);`, + }, }, + ); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + const keys = Object.keys(files).sort(); + assertEquals(keys, ["file:///foo.ts.js", "file:///foo.ts.js.map"]); + }, +}); + +Deno.test({ + name: "Deno.emit() - import maps", + async fn() { + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "/a.ts", { - lib: ["dom", "es2018", "deno.ns"], + importMap: { + imports: { + "b": "./b.ts", + }, + }, + importMapPath: "/import-map.json", + sources: { + "/a.ts": `import * as b from "b" + console.log(b);`, + "/b.ts": `export const b = "b";`, + }, }, ); - assert(diagnostics == null); - assert(actual); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + const keys = Object.keys(files).sort(); assertEquals( - Object.keys(actual).sort(), - ["file:///foo.ts.js", "file:///foo.ts.js.map"], + keys, + [ + "file:///a.ts.js", + "file:///a.ts.js.map", + "file:///b.ts.js", + "file:///b.ts.js.map", + ], ); }, }); -// TODO(@kitsonk) figure the "right way" to restore support for types -// Deno.test({ -// name: "Deno.compile() - properly handles .d.ts files", -// async fn() { -// const [diagnostics, actual] = await Deno.compile( -// "/foo.ts", -// { -// "/foo.ts": `console.log(Foo.bar);`, -// "/foo_types.d.ts": `declare namespace Foo { -// const bar: string; -// }`, -// }, -// { -// types: ["/foo_types.d.ts"], -// }, -// ); -// assert(diagnostics == null); -// assert(actual); -// assertEquals( -// Object.keys(actual).sort(), -// ["file:///foo.ts.js", "file:///file.ts.js.map"], -// ); -// }, -// }); - Deno.test({ - name: "Deno.transpileOnly()", + name: "Deno.emit() - no check", async fn() { - const actual = await Deno.transpileOnly({ - "foo.ts": `export enum Foo { Foo, Bar, Baz };\n`, - }); - assert(actual); - assertEquals(Object.keys(actual), ["foo.ts"]); - assert(actual["foo.ts"].source.startsWith("export var Foo;")); - assert(actual["foo.ts"].map); + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "/foo.ts", + { + check: false, + sources: { + "/foo.ts": `export enum Foo { Foo, Bar, Baz };\n`, + }, + }, + ); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 3); + assertEquals( + Object.keys(files).sort(), + ["file:///foo.ts.js", "file:///foo.ts.js.map"], + ); + assert(files["file:///foo.ts.js"].startsWith("export var Foo;")); }, }); Deno.test({ - name: "Deno.transpileOnly() - config effects commit", + name: "Deno.emit() - no check - config effects emit", async fn() { - const actual = await Deno.transpileOnly( - { - "foo.ts": `/** This is JSDoc */\nexport enum Foo { Foo, Bar, Baz };\n`, - }, + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "/foo.ts", { - removeComments: true, + check: false, + compilerOptions: { removeComments: true }, + sources: { + "/foo.ts": + `/** This is JSDoc */\nexport enum Foo { Foo, Bar, Baz };\n`, + }, }, ); - assert(actual); - assertEquals(Object.keys(actual), ["foo.ts"]); - assert(!actual["foo.ts"].source.includes("This is JSDoc")); - assert(actual["foo.ts"].map); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 3); + assertEquals( + Object.keys(files).sort(), + ["file:///foo.ts.js", "file:///foo.ts.js.map"], + ); + assert(!files["file:///foo.ts.js"].includes("This is JSDoc")); }, }); Deno.test({ - name: "Deno.bundle() - sources passed", + name: "Deno.emit() - bundle esm - with sources", async fn() { - const [diagnostics, actual] = await Deno.bundle("/foo.ts", { - "/foo.ts": `export * from "./bar.ts";\n`, - "/bar.ts": `export const bar = "bar";\n`, - }); - assert(diagnostics == null); - assert(actual.includes(`const bar = "bar"`)); + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "/foo.ts", + { + bundle: "esm", + sources: { + "/foo.ts": `export * from "./bar.ts";\n`, + "/bar.ts": `export const bar = "bar";\n`, + }, + }, + ); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + assertEquals(Object.keys(files), ["deno:///bundle.js"]); + assert(files["deno:///bundle.js"].includes(`const bar = "bar"`)); }, }); Deno.test({ - name: "Deno.bundle() - no sources passed", + name: "Deno.emit() - bundle esm - no sources", async fn() { - const [diagnostics, actual] = await Deno.bundle("./subdir/mod1.ts"); - assert(diagnostics == null); - assert(actual.length); + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "./subdir/mod1.ts", + { + bundle: "esm", + }, + ); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + assertEquals(Object.keys(files), ["deno:///bundle.js"]); + assert(files["deno:///bundle.js"].length); }, }); Deno.test({ - name: "Deno.bundle() - JS Modules included", + name: "Deno.emit() - bundle esm - include js modules", async fn() { - const [diagnostics, actual] = await Deno.bundle("/foo.js", { - "/foo.js": `export * from "./bar.js";\n`, - "/bar.js": `export const bar = "bar";\n`, - }); - assert(diagnostics == null); - assert(actual.includes(`const bar = "bar"`)); + const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( + "/foo.js", + { + bundle: "esm", + sources: { + "/foo.js": `export * from "./bar.js";\n`, + "/bar.js": `export const bar = "bar";\n`, + }, + }, + ); + assertEquals(diagnostics.length, 0); + assert(!ignoredOptions); + assertEquals(stats.length, 12); + assertEquals(Object.keys(files), ["deno:///bundle.js"]); + assert(files["deno:///bundle.js"].includes(`const bar = "bar"`)); }, }); Deno.test({ - name: "runtime compiler APIs diagnostics", + name: "Deno.emit() - generates diagnostics", async fn() { - const [diagnostics] = await Deno.compile("/foo.ts", { - "/foo.ts": `document.getElementById("foo");`, - }); - assert(Array.isArray(diagnostics)); - assert(diagnostics.length === 1); + const { diagnostics, files } = await Deno.emit( + "/foo.ts", + { + sources: { + "/foo.ts": `document.getElementById("foo");`, + }, + }, + ); + assertEquals(diagnostics.length, 1); + assertEquals( + Object.keys(files).sort(), + ["file:///foo.ts.js", "file:///foo.ts.js.map"], + ); }, }); // See https://github.com/denoland/deno/issues/6908 Deno.test({ - name: "Deno.compile() - SWC diagnostics", + name: "Deno.emit() - invalid syntax does not panic", async fn() { await assertThrowsAsync(async () => { - await Deno.compile("/main.js", { - "/main.js": ` - export class Foo { - constructor() { - console.log("foo"); - } - export get() { - console.log("bar"); - } - }`, + await Deno.emit("/main.js", { + sources: { + "/main.js": ` + export class Foo { + constructor() { + console.log("foo"); + } + export get() { + console.log("bar"); + } + }`, + }, }); }); }, }); Deno.test({ - name: `Deno.compile() - Allows setting of "importsNotUsedAsValues"`, + name: 'Deno.emit() - allows setting of "importsNotUsedAsValues"', async fn() { - const [diagnostics] = await Deno.compile("/a.ts", { - "/a.ts": `import { B } from "./b.ts"; - const b: B = { b: "b" }; - `, - "/b.ts": `export interface B { - b: string; - }; - `, - }, { - importsNotUsedAsValues: "error", + const { diagnostics } = await Deno.emit("/a.ts", { + sources: { + "/a.ts": `import { B } from "./b.ts"; + const b: B = { b: "b" };`, + "/b.ts": `export interface B { + b:string; + };`, + }, + compilerOptions: { + importsNotUsedAsValues: "error", + }, }); assert(diagnostics); assertEquals(diagnostics.length, 1); diff --git a/cli/tests/lib_ref.ts b/cli/tests/lib_ref.ts index 7b7bc4ecabbd1d..2454f8b5d0ce36 100644 --- a/cli/tests/lib_ref.ts +++ b/cli/tests/lib_ref.ts @@ -1,14 +1,16 @@ -const [errors, program] = await Deno.compile( +const { diagnostics, files } = await Deno.emit( "/main.ts", { - "/main.ts": - `/// \n\ndocument.getElementById("foo");\nDeno.args;`, - }, - { - target: "es2018", - lib: ["es2018", "deno.ns"], + sources: { + "/main.ts": + `/// \n\ndocument.getElementById("foo");\nDeno.args;`, + }, + compilerOptions: { + target: "es2018", + lib: ["es2018", "deno.ns"], + }, }, ); -console.log(errors); -console.log(Object.keys(program).sort()); +console.log(diagnostics); +console.log(Object.keys(files).sort()); diff --git a/cli/tests/lib_ref.ts.out b/cli/tests/lib_ref.ts.out index 7aee0cc58c60ea..4e0f933fc5a87d 100644 --- a/cli/tests/lib_ref.ts.out +++ b/cli/tests/lib_ref.ts.out @@ -1,2 +1,2 @@ -undefined +[] [ "file:///[WILDCARD]main.ts.js", "file:///[WILDCARD]main.ts.js.map" ] diff --git a/cli/tests/lib_runtime_api.ts b/cli/tests/lib_runtime_api.ts index fc00825e9c0c17..450d9480b8ed0b 100644 --- a/cli/tests/lib_runtime_api.ts +++ b/cli/tests/lib_runtime_api.ts @@ -1,12 +1,14 @@ -const [errors, program] = await Deno.compile( +const { diagnostics, files } = await Deno.emit( "/main.ts", { - "/main.ts": `document.getElementById("foo");`, - }, - { - lib: ["dom", "esnext"], + sources: { + "/main.ts": `document.getElementById("foo");`, + }, + compilerOptions: { + lib: ["dom", "esnext"], + }, }, ); -console.log(errors); -console.log(Object.keys(program).sort()); +console.log(diagnostics); +console.log(Object.keys(files).sort()); diff --git a/cli/tests/lib_runtime_api.ts.out b/cli/tests/lib_runtime_api.ts.out index 7aee0cc58c60ea..4e0f933fc5a87d 100644 --- a/cli/tests/lib_runtime_api.ts.out +++ b/cli/tests/lib_runtime_api.ts.out @@ -1,2 +1,2 @@ -undefined +[] [ "file:///[WILDCARD]main.ts.js", "file:///[WILDCARD]main.ts.js.map" ] diff --git a/cli/tests/subdir/worker_unstable.ts b/cli/tests/subdir/worker_unstable.ts index 3a1ed8b74d7a7e..a5b5f7ba2bef0a 100644 --- a/cli/tests/subdir/worker_unstable.ts +++ b/cli/tests/subdir/worker_unstable.ts @@ -1,5 +1,5 @@ console.log(Deno.permissions.query); -console.log(Deno.compile); +console.log(Deno.emit); self.onmessage = () => { self.close(); }; diff --git a/cli/tests/unstable_worker.ts.out b/cli/tests/unstable_worker.ts.out index cdbb7e91119d51..ece47de97d73fe 100644 --- a/cli/tests/unstable_worker.ts.out +++ b/cli/tests/unstable_worker.ts.out @@ -1,2 +1,2 @@ [Function: query] -[AsyncFunction: compile] +[Function: emit] diff --git a/cli/tsc_config.rs b/cli/tsc_config.rs index 16661c7680591f..2409106fd849af 100644 --- a/cli/tsc_config.rs +++ b/cli/tsc_config.rs @@ -49,6 +49,15 @@ impl fmt::Display for IgnoredCompilerOptions { } } +impl Serialize for IgnoredCompilerOptions { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Serialize::serialize(&self.items, serializer) + } +} + /// A static slice of all the compiler options that should be ignored that /// either have no effect on the compilation or would cause the emit to not work /// in Deno. @@ -246,6 +255,14 @@ impl TsConfig { } } + pub fn get_declaration(&self) -> bool { + if let Some(declaration) = self.0.get("declaration") { + declaration.as_bool().unwrap_or(false) + } else { + false + } + } + /// Merge a serde_json value into the configuration. pub fn merge(&mut self, value: &Value) { json_merge(&mut self.0, value); diff --git a/runtime/rt/40_compiler_api.js b/runtime/rt/40_compiler_api.js index ea963b67b9c342..ced4bcb001fe40 100644 --- a/runtime/rt/40_compiler_api.js +++ b/runtime/rt/40_compiler_api.js @@ -1,97 +1,88 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// @ts-check + // This file contains the runtime APIs which will dispatch work to the internal // compiler within Deno. ((window) => { const core = window.Deno.core; const util = window.__bootstrap.util; - function opCompile(request) { - return core.jsonOpAsync("op_compile", request); - } + /** + * @typedef {object} ImportMap + * @property {Record} imports + * @property {Record>=} scopes + */ + + /** + * @typedef {object} OpEmitRequest + * @property {"esm"=} bundle + * @property {boolean=} check + * @property {Record=} compilerOptions + * @property {ImportMap=} importMap + * @property {string=} importMapPath + * @property {string} rootSpecifier + * @property {Record=} sources + */ + + /** + * @typedef OpEmitResponse + * @property {any[]} diagnostics + * @property {Record} files + * @property {string[]=} ignoredOptions + * @property {Array<[string, number]>} stats + */ - function opTranspile( - request, - ) { - return core.jsonOpAsync("op_transpile", request); + /** + * @param {OpEmitRequest} request + * @returns {Promise} + */ + function opEmit(request) { + return core.jsonOpAsync("op_emit", request); } + /** + * @param {string} specifier + * @returns {string} + */ function checkRelative(specifier) { return specifier.match(/^([\.\/\\]|https?:\/{2}|file:\/{2})/) ? specifier : `./${specifier}`; } - // TODO(bartlomieju): change return type to interface? - function transpileOnly( - sources, - options = {}, - ) { - util.log("Deno.transpileOnly", { sources: Object.keys(sources), options }); - const payload = { - sources, - options: JSON.stringify(options), - }; - return opTranspile(payload); - } - - // TODO(bartlomieju): change return type to interface? - async function compile( - rootName, - sources, - options = {}, - ) { - const payload = { - rootName: sources ? rootName : checkRelative(rootName), - sources, - options: JSON.stringify(options), - bundle: false, - }; - util.log("Deno.compile", { - rootName: payload.rootName, - sources: !!sources, - options, - }); - /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ - const result = await opCompile(payload); - util.assert(result.emittedFiles); - const maybeDiagnostics = result.diagnostics.length === 0 - ? undefined - : result.diagnostics; - - return [maybeDiagnostics, result.emittedFiles]; - } + /** + * @typedef {object} EmitOptions + * @property {"esm"=} bundle + * @property {boolean=} check + * @property {Record=} compilerOptions + * @property {ImportMap=} importMap + * @property {string=} importMapPath + * @property {Record=} sources + */ - // TODO(bartlomieju): change return type to interface? - async function bundle( - rootName, - sources, - options = {}, - ) { - const payload = { - rootName: sources ? rootName : checkRelative(rootName), - sources, - options: JSON.stringify(options), - bundle: true, - }; - util.log("Deno.bundle", { - rootName: payload.rootName, - sources: !!sources, - options, - }); - /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ - const result = await opCompile(payload); - const output = result.emittedFiles["deno:///bundle.js"]; - util.assert(output); - const maybeDiagnostics = result.diagnostics.length === 0 - ? undefined - : result.diagnostics; - return [maybeDiagnostics, output]; + /** + * @param {string | URL} rootSpecifier + * @param {EmitOptions=} options + * @returns {Promise} + */ + function emit(rootSpecifier, options = {}) { + util.log(`Deno.emit`, { rootSpecifier }); + if (!rootSpecifier) { + return Promise.reject( + new TypeError("A root specifier must be supplied."), + ); + } + if (!(typeof rootSpecifier === "string")) { + rootSpecifier = rootSpecifier.toString(); + } + if (!options.sources) { + rootSpecifier = checkRelative(rootSpecifier); + } + return opEmit({ rootSpecifier, ...options }); } window.__bootstrap.compilerApi = { - bundle, - compile, - transpileOnly, + emit, }; })(this); diff --git a/runtime/rt/90_deno_ns.js b/runtime/rt/90_deno_ns.js index 9188788ec75f16..c71f175e484a50 100644 --- a/runtime/rt/90_deno_ns.js +++ b/runtime/rt/90_deno_ns.js @@ -94,9 +94,7 @@ signals: __bootstrap.signals.signals, Signal: __bootstrap.signals.Signal, SignalStream: __bootstrap.signals.SignalStream, - transpileOnly: __bootstrap.compilerApi.transpileOnly, - compile: __bootstrap.compilerApi.compile, - bundle: __bootstrap.compilerApi.bundle, + emit: __bootstrap.compilerApi.emit, permissions: __bootstrap.permissions.permissions, Permissions: __bootstrap.permissions.Permissions, PermissionStatus: __bootstrap.permissions.PermissionStatus,