refactor(cli): runtime compiler APIs consolidated to Deno.emit() (#8799)

Closes: #4752
This commit is contained in:
Kitson Kelly 2021-01-01 08:43:54 +11:00 committed by GitHub
parent 5f4e1767fe
commit 012f99bd9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 592 additions and 516 deletions

View File

@ -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",

View File

@ -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;
@ -338,9 +340,6 @@ declare namespace Deno {
/** Emit the source alongside the source maps within a single file; requires
* `inlineSourceMap` or `sourceMap` to be set. Defaults to `false`. */
inlineSources?: boolean;
/** Perform additional checks to ensure that transpile only would be safe.
* Defaults to `true`. */
isolatedModules?: boolean;
/** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`.
* Defaults to `"react"`. */
jsx?: "react" | "preserve" | "react-native";
@ -393,12 +392,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 +491,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<string, string>;
scopes?: Record<string, Record<string, string>>;
}
/** **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<string, string>,
options?: CompilerOptions,
): Promise<Record<string, TranspileOnlyResult>>;
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<string, string>;
}
/** **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<string, string>,
options?: CompilerOptions,
): Promise<[Diagnostic[] | undefined, Record<string, string>]>;
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<string, string>;
/** An optional array of any compiler options that were ignored by Deno. */
ignoredOptions?: string[];
/** An array of internal statistics 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 in line with the behavior of the command line.
*
* 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<string, string>,
options?: CompilerOptions,
): Promise<[Diagnostic[] | undefined, string]>;
export function emit(
rootSpecifier: string | URL,
options?: EmitOptions,
): Promise<EmitResult>;
/** **UNSTABLE**: Should not have same name as `window.location` type. */
interface Location {

View File

@ -497,18 +497,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<D>(deserializer: D) -> result::Result<Self, D::Error>
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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:")?;
@ -620,6 +629,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.
@ -769,8 +782,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))
@ -918,18 +931,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<String, String>, 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,
@ -937,11 +954,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://",
@ -957,74 +970,138 @@ 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 = Arc::new(Mutex::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.lock().unwrap();
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 = Arc::new(Mutex::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.lock().unwrap();
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());
}
let parsed_module = module.parse()?;
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()`.
@ -1566,10 +1643,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
@ -1604,9 +1680,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 {
@ -2270,6 +2346,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,
@ -2310,6 +2387,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,
@ -2347,6 +2425,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),

View File

@ -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,11 +8,10 @@ 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_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;
@ -30,37 +27,46 @@ use std::sync::Arc;
use std::sync::Mutex;
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<RuntimeBundleType>,
check: Option<bool>,
compiler_options: Option<HashMap<String, Value>>,
import_map: Option<Value>,
import_map_path: Option<String>,
root_specifier: String,
sources: Option<HashMap<String, String>>,
bundle: bool,
options: Option<String>,
}
async fn op_compile(
async fn op_emit(
state: Rc<RefCell<OpState>>,
args: Value,
_data: BufVec,
) -> Result<Value, AnyError> {
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::<Arc<ProgramState>>().clone();
let runtime_permissions = {
let state = state.borrow();
state.borrow::<Permissions>().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: Arc<Mutex<dyn SpecifierHandler>> =
if let Some(sources) = args.sources {
is_dynamic = true;
Arc::new(Mutex::new(MemoryHandler::new(sources)))
} else {
Arc::new(Mutex::new(FetchHandler::new(
@ -68,93 +74,44 @@ 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
} else {
BundleType::None
};
let debug = program_state.flags.log_level == Some(log::Level::Debug);
let maybe_user_config: Option<HashMap<String, Value>> =
if let Some(options) = args.options {
Some(serde_json::from_str(&options)?)
let maybe_import_map = if let Some(import_map_str) = args.import_map_path {
let import_map_specifier =
ModuleSpecifier::resolve_url_or_path(&import_map_str).context(
format!("Bad file path (\"{}\") for import map.", import_map_str),
)?;
let import_map_url = import_map_specifier.as_url();
let import_map = if let Some(value) = args.import_map {
ImportMap::from_json(&import_map_url.to_string(), &value.to_string())?
} else {
None
ImportMap::load(&import_map_str)?
};
let (emitted_files, result_info) = graph.emit(EmitOptions {
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 {
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 (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<String, String>,
options: Option<String>,
}
#[derive(Debug, Serialize)]
struct RuntimeTranspileEmit {
source: String,
map: Option<String>,
}
async fn op_transpile(
state: Rc<RefCell<OpState>>,
args: Value,
_data: BufVec,
) -> Result<Value, AnyError> {
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<String, Value> = 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)
}

View File

@ -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,246 @@ 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);`,
},
{
lib: ["dom", "es2018", "deno.ns"],
compilerOptions: {
lib: ["dom", "es2018", "deno.ns"],
},
sources: {
"file:///foo.ts": `console.log(document.getElementById("foo"));
console.log(Deno.args);`,
},
},
);
assert(diagnostics == null);
assert(actual);
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(
"file:///a.ts",
{
importMap: {
imports: {
"b": "./b.ts",
},
},
importMapPath: "file:///import-map.json",
sources: {
"file:///a.ts": `import * as b from "b"
console.log(b);`,
"file:///b.ts": `export const b = "b";`,
},
},
);
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);
},
});
Deno.test({
name: "Deno.transpileOnly() - config effects commit",
async fn() {
const actual = await Deno.transpileOnly(
const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
"/foo.ts",
{
"foo.ts": `/** This is JSDoc */\nexport enum Foo { Foo, Bar, Baz };\n`,
},
{
removeComments: true,
check: false,
sources: {
"/foo.ts": `export 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);
const keys = Object.keys(files).sort();
assert(keys[0].endsWith("/foo.ts.js"));
assert(keys[1].endsWith("/foo.ts.js.map"));
assert(files[keys[0]].startsWith("export var Foo;"));
},
});
Deno.test({
name: "Deno.bundle() - sources passed",
name: "Deno.emit() - no check - config effects emit",
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",
{
check: false,
compilerOptions: { removeComments: true },
sources: {
"/foo.ts":
`/** This is JSDoc */\nexport enum Foo { Foo, Bar, Baz };\n`,
},
},
);
assertEquals(diagnostics.length, 0);
assert(!ignoredOptions);
assertEquals(stats.length, 3);
const keys = Object.keys(files).sort();
assert(keys[0].endsWith("/foo.ts.js"));
assert(keys[1].endsWith("/foo.ts.js.map"));
assert(!files[keys[0]].includes("This is JSDoc"));
},
});
Deno.test({
name: "Deno.bundle() - no sources passed",
name: "Deno.emit() - bundle esm - with 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(
"/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() - JS Modules included",
name: "Deno.emit() - bundle esm - no sources",
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(
"./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: "runtime compiler APIs diagnostics",
name: "Deno.emit() - bundle esm - include js modules",
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, 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: "Deno.emit() - generates diagnostics",
async fn() {
const { diagnostics, files } = await Deno.emit(
"/foo.ts",
{
sources: {
"/foo.ts": `document.getElementById("foo");`,
},
},
);
assertEquals(diagnostics.length, 1);
const keys = Object.keys(files).sort();
assert(keys[0].endsWith("/foo.ts.js"));
assert(keys[1].endsWith("/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);

View File

@ -1,14 +1,16 @@
const [errors, program] = await Deno.compile(
const { diagnostics, files } = await Deno.emit(
"/main.ts",
{
"/main.ts":
`/// <reference lib="dom" />\n\ndocument.getElementById("foo");\nDeno.args;`,
},
{
target: "es2018",
lib: ["es2018", "deno.ns"],
sources: {
"/main.ts":
`/// <reference lib="dom" />\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());

View File

@ -1,2 +1,2 @@
undefined
[]
[ "file:///[WILDCARD]main.ts.js", "file:///[WILDCARD]main.ts.js.map" ]

View File

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

View File

@ -1,2 +1,2 @@
undefined
[]
[ "file:///[WILDCARD]main.ts.js", "file:///[WILDCARD]main.ts.js.map" ]

View File

@ -1,5 +1,5 @@
console.log(Deno.permissions.query);
console.log(Deno.compile);
console.log(Deno.emit);
self.onmessage = () => {
self.close();
};

View File

@ -1,2 +1,2 @@
[Function: query]
[AsyncFunction: compile]
[Function: emit]

View File

@ -49,6 +49,15 @@ impl fmt::Display for IgnoredCompilerOptions {
}
}
impl Serialize for IgnoredCompilerOptions {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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.
@ -64,7 +73,6 @@ pub const IGNORED_COMPILER_OPTIONS: &[&str] = &[
"importHelpers",
"inlineSourceMap",
"inlineSources",
"isolatedModules",
"module",
"noEmitHelpers",
"noLib",
@ -97,6 +105,7 @@ pub const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[
"help",
"incremental",
"init",
"isolatedModules",
"listEmittedFiles",
"listFiles",
"mapRoot",
@ -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);

View File

@ -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);
}
function opTranspile(
request,
) {
return core.jsonOpAsync("op_transpile", request);
/**
* @typedef {object} ImportMap
* @property {Record<string, string>} imports
* @property {Record<string, Record<string, string>>=} scopes
*/
/**
* @typedef {object} OpEmitRequest
* @property {"esm"=} bundle
* @property {boolean=} check
* @property {Record<string, any>=} compilerOptions
* @property {ImportMap=} importMap
* @property {string=} importMapPath
* @property {string} rootSpecifier
* @property {Record<string, string>=} sources
*/
/**
* @typedef OpEmitResponse
* @property {any[]} diagnostics
* @property {Record<string, string>} files
* @property {string[]=} ignoredOptions
* @property {Array<[string, number]>} stats
*/
/**
* @param {OpEmitRequest} request
* @returns {Promise<OpEmitResponse>}
*/
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);
}
/**
* @typedef {object} EmitOptions
* @property {"esm"=} bundle
* @property {boolean=} check
* @property {Record<string, any>=} compilerOptions
* @property {ImportMap=} importMap
* @property {string=} importMapPath
* @property {Record<string, string>=} sources
*/
// 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<string, string>, diagnostics: any[] }} */
const result = await opCompile(payload);
util.assert(result.emittedFiles);
const maybeDiagnostics = result.diagnostics.length === 0
? undefined
: result.diagnostics;
return [maybeDiagnostics, result.emittedFiles];
}
// 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<string, string>, 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<OpEmitResponse>}
*/
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);

View File

@ -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,