diff --git a/cli/compilers/mod.rs b/cli/compilers/mod.rs index c4e2b2e6b98aad..87e34ac5fc1a71 100644 --- a/cli/compilers/mod.rs +++ b/cli/compilers/mod.rs @@ -1,6 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use deno_core::ErrBox; use futures::Future; +use serde_json::Value; mod js; mod json; @@ -9,9 +10,14 @@ mod wasm; pub use js::JsCompiler; pub use json::JsonCompiler; +pub use ts::runtime_compile_async; +pub use ts::runtime_transpile_async; pub use ts::TsCompiler; pub use wasm::WasmCompiler; +pub type CompilationResultFuture = + dyn Future> + Send; + #[derive(Debug, Clone)] pub struct CompiledModule { pub code: String, diff --git a/cli/compilers/ts.rs b/cli/compilers/ts.rs index 5a9117d59c67b1..e8abbcd272c6ec 100644 --- a/cli/compilers/ts.rs +++ b/cli/compilers/ts.rs @@ -1,4 +1,5 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::compilers::CompilationResultFuture; use crate::compilers::CompiledModule; use crate::compilers::CompiledModuleFuture; use crate::diagnostics::Diagnostic; @@ -7,6 +8,7 @@ use crate::file_fetcher::SourceFile; use crate::file_fetcher::SourceFileFetcher; use crate::global_state::ThreadSafeGlobalState; use crate::msg; +use crate::serde_json::json; use crate::source_maps::SourceMapGetter; use crate::startup_data; use crate::state::*; @@ -18,8 +20,10 @@ use deno_core::ModuleSpecifier; use futures::future::FutureExt; use futures::Future; use regex::Regex; +use std::collections::HashMap; use std::collections::HashSet; use std::fs; +use std::hash::BuildHasher; use std::io; use std::path::PathBuf; use std::pin::Pin; @@ -156,12 +160,14 @@ fn req( root_names: Vec, compiler_config: CompilerConfig, out_file: Option, + bundle: bool, ) -> Buf { let j = match (compiler_config.path, compiler_config.content) { (Some(config_path), Some(config_data)) => json!({ "type": request_type as i32, "rootNames": root_names, "outFile": out_file, + "bundle": bundle, "configPath": config_path, "config": str::from_utf8(&config_data).unwrap(), }), @@ -169,6 +175,7 @@ fn req( "type": request_type as i32, "rootNames": root_names, "outFile": out_file, + "bundle": bundle, }), }; @@ -258,10 +265,11 @@ impl TsCompiler { let root_names = vec![module_name]; let req_msg = req( - msg::CompilerRequestType::Bundle, + msg::CompilerRequestType::Compile, root_names, self.config.clone(), out_file, + true, ); let worker = TsCompiler::setup_worker(global_state); @@ -356,6 +364,7 @@ impl TsCompiler { root_names, self.config.clone(), None, + false, ); let worker = TsCompiler::setup_worker(global_state.clone()); @@ -599,6 +608,66 @@ impl TsCompiler { } } +pub fn runtime_compile_async( + global_state: ThreadSafeGlobalState, + root_name: &str, + sources: &Option>, + bundle: bool, + options: &Option, +) -> Pin> { + let req_msg = json!({ + "type": msg::CompilerRequestType::RuntimeCompile as i32, + "rootName": root_name, + "sources": sources, + "options": options, + "bundle": bundle, + }) + .to_string() + .into_boxed_str() + .into_boxed_bytes(); + + let worker = TsCompiler::setup_worker(global_state); + let worker_ = worker.clone(); + + async move { + worker.post_message(req_msg).await?; + worker.await?; + debug!("Sent message to worker"); + let msg = (worker_.get_message().await?).unwrap(); + let json_str = std::str::from_utf8(&msg).unwrap(); + Ok(json!(json_str)) + } + .boxed() +} + +pub fn runtime_transpile_async( + global_state: ThreadSafeGlobalState, + sources: &HashMap, + options: &Option, +) -> Pin> { + let req_msg = json!({ + "type": msg::CompilerRequestType::RuntimeTranspile as i32, + "sources": sources, + "options": options, + }) + .to_string() + .into_boxed_str() + .into_boxed_bytes(); + + let worker = TsCompiler::setup_worker(global_state); + let worker_ = worker.clone(); + + async move { + worker.post_message(req_msg).await?; + worker.await?; + debug!("Sent message to worker"); + let msg = (worker_.get_message().await?).unwrap(); + let json_str = std::str::from_utf8(&msg).unwrap(); + Ok(json!(json_str)) + } + .boxed() +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/js/compiler.ts b/cli/js/compiler.ts index a393faac8e8b99..0d9cf83d9af535 100644 --- a/cli/js/compiler.ts +++ b/cli/js/compiler.ts @@ -1,711 +1,301 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // TODO(ry) Combine this implementation with //deno_typescript/compiler_main.js +// these are imported for their side effects import "./globals.ts"; import "./ts_global.d.ts"; -import { emitBundle, setRootExports } from "./bundler.ts"; -import { bold, cyan, yellow } from "./colors.ts"; -import { Console } from "./console.ts"; -import { core } from "./core.ts"; -import { Diagnostic, fromTypeScriptDiagnostic } from "./diagnostics.ts"; -import { cwd } from "./dir.ts"; -import * as dispatch from "./dispatch.ts"; -import { sendAsync, sendSync } from "./dispatch_json.ts"; +import { TranspileOnlyResult } from "./compiler_api.ts"; +import { setRootExports } from "./compiler_bundler.ts"; +import { + defaultBundlerOptions, + defaultRuntimeCompileOptions, + defaultTranspileOptions, + Host +} from "./compiler_host.ts"; +import { + processImports, + processLocalImports, + resolveModules +} from "./compiler_imports.ts"; +import { + createWriteFile, + CompilerRequestType, + convertCompilerOptions, + ignoredDiagnostics, + WriteFileState, + processConfigureResponse +} from "./compiler_util.ts"; +import { Diagnostic } from "./diagnostics.ts"; +import { fromTypeScriptDiagnostic } from "./diagnostics_util.ts"; import * as os from "./os.ts"; -import { getMappedModuleName, parseTypeDirectives } from "./type_directives.ts"; -import { assert, notImplemented } from "./util.ts"; +import { assert } from "./util.ts"; import * as util from "./util.ts"; -import { window } from "./window.ts"; +import { window as self } from "./window.ts"; import { postMessage, workerClose, workerMain } from "./workers.ts"; -// Warning! The values in this enum are duplicated in cli/msg.rs -// Update carefully! -enum MediaType { - JavaScript = 0, - JSX = 1, - TypeScript = 2, - TSX = 3, - Json = 4, - Wasm = 5, - Unknown = 6 -} - -// Warning! The values in this enum are duplicated in cli/msg.rs -// Update carefully! -enum CompilerRequestType { - Compile = 0, - Bundle = 1 -} - -// Startup boilerplate. This is necessary because the compiler has its own -// snapshot. (It would be great if we could remove these things or centralize -// them somewhere else.) -const console = new Console(core.print); -window.console = console; -window.workerMain = workerMain; -function denoMain(compilerType?: string): void { - os.start(true, compilerType || "TS"); -} -window["denoMain"] = denoMain; - -const ASSETS = "$asset$"; -const OUT_DIR = "$deno$"; - -/** The format of the work message payload coming from the privileged side */ -type CompilerRequest = { +interface CompilerRequestCompile { + type: CompilerRequestType.Compile; rootNames: string[]; // TODO(ry) add compiler config to this interface. // options: ts.CompilerOptions; configPath?: string; config?: string; -} & ( - | { - type: CompilerRequestType.Compile; - } - | { - type: CompilerRequestType.Bundle; - outFile?: string; - } -); - -interface ConfigureResponse { - ignoredOptions?: string[]; - diagnostics?: ts.Diagnostic[]; + bundle?: boolean; + outFile?: string; } -/** Options that either do nothing in Deno, or would cause undesired behavior - * if modified. */ -const ignoredCompilerOptions: readonly string[] = [ - "allowSyntheticDefaultImports", - "baseUrl", - "build", - "composite", - "declaration", - "declarationDir", - "declarationMap", - "diagnostics", - "downlevelIteration", - "emitBOM", - "emitDeclarationOnly", - "esModuleInterop", - "extendedDiagnostics", - "forceConsistentCasingInFileNames", - "help", - "importHelpers", - "incremental", - "inlineSourceMap", - "inlineSources", - "init", - "isolatedModules", - "lib", - "listEmittedFiles", - "listFiles", - "mapRoot", - "maxNodeModuleJsDepth", - "module", - "moduleResolution", - "newLine", - "noEmit", - "noEmitHelpers", - "noEmitOnError", - "noLib", - "noResolve", - "out", - "outDir", - "outFile", - "paths", - "preserveSymlinks", - "preserveWatchOutput", - "pretty", - "rootDir", - "rootDirs", - "showConfig", - "skipDefaultLibCheck", - "skipLibCheck", - "sourceMap", - "sourceRoot", - "stripInternal", - "target", - "traceResolution", - "tsBuildInfoFile", - "types", - "typeRoots", - "version", - "watch" -]; - -/** The shape of the SourceFile that comes from the privileged side */ -interface SourceFileJson { - url: string; - filename: string; - mediaType: MediaType; - sourceCode: string; +interface CompilerRequestRuntimeCompile { + type: CompilerRequestType.RuntimeCompile; + rootName: string; + sources?: Record; + bundle?: boolean; + options?: string; } -/** A self registering abstraction of source files. */ -class SourceFile { - extension!: ts.Extension; - filename!: string; - - /** An array of tuples which represent the imports for the source file. The - * first element is the one that will be requested at compile time, the - * second is the one that should be actually resolved. This provides the - * feature of type directives for Deno. */ - importedFiles?: Array<[string, string]>; - - mediaType!: MediaType; - processed = false; - sourceCode!: string; - tsSourceFile?: ts.SourceFile; - url!: string; - - constructor(json: SourceFileJson) { - if (SourceFile._moduleCache.has(json.url)) { - throw new TypeError("SourceFile already exists"); - } - Object.assign(this, json); - this.extension = getExtension(this.url, this.mediaType); - SourceFile._moduleCache.set(this.url, this); - } - - /** Cache the source file to be able to be retrieved by `moduleSpecifier` and - * `containingFile`. */ - cache(moduleSpecifier: string, containingFile?: string): void { - containingFile = containingFile || ""; - let innerCache = SourceFile._specifierCache.get(containingFile); - if (!innerCache) { - innerCache = new Map(); - SourceFile._specifierCache.set(containingFile, innerCache); - } - innerCache.set(moduleSpecifier, this); - } - - /** Process the imports for the file and return them. */ - imports(): Array<[string, string]> { - if (this.processed) { - throw new Error("SourceFile has already been processed."); - } - assert(this.sourceCode != null); - // we shouldn't process imports for files which contain the nocheck pragma - // (like bundles) - if (this.sourceCode.match(/\/{2}\s+@ts-nocheck/)) { - util.log(`Skipping imports for "${this.filename}"`); - return []; - } - const preProcessedFileInfo = ts.preProcessFile(this.sourceCode, true, true); - this.processed = true; - const files = (this.importedFiles = [] as Array<[string, string]>); - - function process(references: ts.FileReference[]): void { - for (const { fileName } of references) { - files.push([fileName, fileName]); - } - } - - const { - importedFiles, - referencedFiles, - libReferenceDirectives, - typeReferenceDirectives - } = preProcessedFileInfo; - const typeDirectives = parseTypeDirectives(this.sourceCode); - if (typeDirectives) { - for (const importedFile of importedFiles) { - files.push([ - importedFile.fileName, - getMappedModuleName(importedFile, typeDirectives) - ]); - } - } else { - process(importedFiles); - } - process(referencedFiles); - process(libReferenceDirectives); - process(typeReferenceDirectives); - return files; - } - - /** A cache of all the source files which have been loaded indexed by the - * url. */ - private static _moduleCache: Map = new Map(); - - /** A cache of source files based on module specifiers and containing files - * which is used by the TypeScript compiler to resolve the url */ - private static _specifierCache: Map< - string, - Map - > = new Map(); - - /** Retrieve a `SourceFile` based on a `moduleSpecifier` and `containingFile` - * or return `undefined` if not preset. */ - static getUrl( - moduleSpecifier: string, - containingFile: string - ): string | undefined { - const containingCache = this._specifierCache.get(containingFile); - if (containingCache) { - const sourceFile = containingCache.get(moduleSpecifier); - return sourceFile && sourceFile.url; - } - return undefined; - } - - /** Retrieve a `SourceFile` based on a `url` */ - static get(url: string): SourceFile | undefined { - return this._moduleCache.get(url); - } +interface CompilerRequestRuntimeTranspile { + type: CompilerRequestType.RuntimeTranspile; + sources: Record; + options?: string; } -interface EmitResult { +/** The format of the work message payload coming from the privileged side */ +type CompilerRequest = + | CompilerRequestCompile + | CompilerRequestRuntimeCompile + | CompilerRequestRuntimeTranspile; + +/** The format of the result sent back when doing a compilation. */ +interface CompileResult { emitSkipped: boolean; diagnostics?: Diagnostic; } -/** Ops to Rust to resolve special static assets. */ -function fetchAsset(name: string): string { - return sendSync(dispatch.OP_FETCH_ASSET, { name }); -} - -/** Ops to Rust to resolve and fetch modules meta data. */ -function fetchSourceFiles( - specifiers: string[], - referrer?: string -): Promise { - util.log("compiler::fetchSourceFiles", { specifiers, referrer }); - return sendAsync(dispatch.OP_FETCH_SOURCE_FILES, { - specifiers, - referrer - }); -} - -/** Recursively process the imports of modules, generating `SourceFile`s of any - * imported files. - * - * Specifiers are supplied in an array of tupples where the first is the - * specifier that will be requested in the code and the second is the specifier - * that should be actually resolved. */ -async function processImports( - specifiers: Array<[string, string]>, - referrer?: string -): Promise { - if (!specifiers.length) { - return []; - } - const sources = specifiers.map(([, moduleSpecifier]) => moduleSpecifier); - const sourceFiles = await fetchSourceFiles(sources, referrer); - assert(sourceFiles.length === specifiers.length); - for (let i = 0; i < sourceFiles.length; i++) { - const sourceFileJson = sourceFiles[i]; - const sourceFile = - SourceFile.get(sourceFileJson.url) || new SourceFile(sourceFileJson); - sourceFile.cache(specifiers[i][0], referrer); - if (!sourceFile.processed) { - await processImports(sourceFile.imports(), sourceFile.url); - } - } - return sourceFiles; -} - -/** Ops to rest for caching source map and compiled js */ -function cache(extension: string, moduleId: string, contents: string): void { - util.log("compiler::cache", { extension, moduleId }); - sendSync(dispatch.OP_CACHE, { extension, moduleId, contents }); -} - -/** Returns the TypeScript Extension enum for a given media type. */ -function getExtension(fileName: string, mediaType: MediaType): ts.Extension { - switch (mediaType) { - case MediaType.JavaScript: - return ts.Extension.Js; - case MediaType.JSX: - return ts.Extension.Jsx; - case MediaType.TypeScript: - return fileName.endsWith(".d.ts") ? ts.Extension.Dts : ts.Extension.Ts; - case MediaType.TSX: - return ts.Extension.Tsx; - case MediaType.Json: - return ts.Extension.Json; - case MediaType.Wasm: - // Custom marker for Wasm type. - return ts.Extension.Js; - case MediaType.Unknown: - default: - throw TypeError("Cannot resolve extension."); - } -} - -class Host implements ts.CompilerHost { - private readonly _options: ts.CompilerOptions = { - allowJs: true, - allowNonTsExtensions: true, - // TODO(#3324) Enable strict mode for user code. - // strict: true, - checkJs: false, - esModuleInterop: true, - module: ts.ModuleKind.ESNext, - outDir: OUT_DIR, - resolveJsonModule: true, - sourceMap: true, - stripComments: true, - target: ts.ScriptTarget.ESNext, - jsx: ts.JsxEmit.React - }; - - private _getAsset(filename: string): SourceFile { - const sourceFile = SourceFile.get(filename); - if (sourceFile) { - return sourceFile; - } - const url = filename.split("/").pop()!; - const assetName = url.includes(".") ? url : `${url}.d.ts`; - const sourceCode = fetchAsset(assetName); - return new SourceFile({ - url, - filename, - mediaType: MediaType.TypeScript, - sourceCode - }); - } - - /* Deno specific APIs */ - - /** Provides the `ts.HostCompiler` interface for Deno. - * - * @param _rootNames A set of modules that are the ones that should be - * instantiated first. Used when generating a bundle. - * @param _bundle Set to a string value to configure the host to write out a - * bundle instead of caching individual files. - */ - constructor( - private _requestType: CompilerRequestType, - private _rootNames: string[], - private _outFile?: string - ) { - if (this._requestType === CompilerRequestType.Bundle) { - // options we need to change when we are generating a bundle - const bundlerOptions: ts.CompilerOptions = { - module: ts.ModuleKind.AMD, - outDir: undefined, - outFile: `${OUT_DIR}/bundle.js`, - // disabled until we have effective way to modify source maps - sourceMap: false - }; - Object.assign(this._options, bundlerOptions); - } - } - - /** Take a configuration string, parse it, and use it to merge with the - * compiler's configuration options. The method returns an array of compiler - * options which were ignored, or `undefined`. */ - configure(path: string, configurationText: string): ConfigureResponse { - util.log("compiler::host.configure", path); - const { config, error } = ts.parseConfigFileTextToJson( - path, - configurationText - ); - if (error) { - return { diagnostics: [error] }; - } - const { options, errors } = ts.convertCompilerOptionsFromJson( - config.compilerOptions, - cwd() - ); - const ignoredOptions: string[] = []; - for (const key of Object.keys(options)) { - if ( - ignoredCompilerOptions.includes(key) && - (!(key in this._options) || options[key] !== this._options[key]) - ) { - ignoredOptions.push(key); - delete options[key]; - } - } - Object.assign(this._options, options); - return { - ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined, - diagnostics: errors.length ? errors : undefined - }; - } - - /* TypeScript CompilerHost APIs */ - - fileExists(_fileName: string): boolean { - return notImplemented(); - } - - getCanonicalFileName(fileName: string): string { - return fileName; - } - - getCompilationSettings(): ts.CompilerOptions { - util.log("compiler::host.getCompilationSettings()"); - return this._options; - } - - getCurrentDirectory(): string { - return ""; - } - - getDefaultLibFileName(_options: ts.CompilerOptions): string { - return ASSETS + "/lib.deno_runtime.d.ts"; - } - - getNewLine(): string { - return "\n"; - } - - getSourceFile( - fileName: string, - languageVersion: ts.ScriptTarget, - onError?: (message: string) => void, - shouldCreateNewSourceFile?: boolean - ): ts.SourceFile | undefined { - util.log("compiler::host.getSourceFile", fileName); - try { - assert(!shouldCreateNewSourceFile); - const sourceFile = fileName.startsWith(ASSETS) - ? this._getAsset(fileName) - : SourceFile.get(fileName); - assert(sourceFile != null); - if (!sourceFile.tsSourceFile) { - sourceFile.tsSourceFile = ts.createSourceFile( - fileName, - sourceFile.sourceCode, - languageVersion - ); - } - return sourceFile!.tsSourceFile; - } catch (e) { - if (onError) { - onError(String(e)); - } else { - throw e; - } - return undefined; - } - } - - readFile(_fileName: string): string | undefined { - return notImplemented(); - } - - resolveModuleNames( - moduleNames: string[], - containingFile: string - ): Array { - util.log("compiler::host.resolveModuleNames", { - moduleNames, - containingFile - }); - return moduleNames.map(specifier => { - const url = SourceFile.getUrl(specifier, containingFile); - const sourceFile = specifier.startsWith(ASSETS) - ? this._getAsset(specifier) - : url - ? SourceFile.get(url) - : undefined; - if (!sourceFile) { - return undefined; - } - return { - resolvedFileName: sourceFile.url, - isExternalLibraryImport: specifier.startsWith(ASSETS), - extension: sourceFile.extension - }; - }); - } - - useCaseSensitiveFileNames(): boolean { - return true; - } - - writeFile( - fileName: string, - data: string, - _writeByteOrderMark: boolean, - onError?: (message: string) => void, - sourceFiles?: readonly ts.SourceFile[] - ): void { - util.log("compiler::host.writeFile", fileName); - try { - assert(sourceFiles != null); - if (this._requestType === CompilerRequestType.Bundle) { - emitBundle(this._rootNames, this._outFile, data, sourceFiles); - } else { - assert(sourceFiles.length == 1); - const url = sourceFiles[0].fileName; - const sourceFile = SourceFile.get(url); - - if (sourceFile) { - // NOTE: If it's a `.json` file we don't want to write it to disk. - // JSON files are loaded and used by TS compiler to check types, but we don't want - // to emit them to disk because output file is the same as input file. - if (sourceFile.extension === ts.Extension.Json) { - return; - } - - // NOTE: JavaScript files are only emitted to disk if `checkJs` option in on - if ( - sourceFile.extension === ts.Extension.Js && - !this._options.checkJs - ) { - return; - } - } +// bootstrap the runtime environment, this gets called as the isolate is setup +self.denoMain = function denoMain(compilerType?: string): void { + os.start(true, compilerType || "TS"); +}; - if (fileName.endsWith(".map")) { - // Source Map - cache(".map", url, data); - } else if (fileName.endsWith(".js") || fileName.endsWith(".json")) { - // Compiled JavaScript - cache(".js", url, data); - } else { - assert(false, "Trying to cache unhandled file type " + fileName); - } - } - } catch (e) { - if (onError) { - onError(String(e)); - } else { - throw e; - } - } - } -} +// bootstrap the worker environment, this gets called as the isolate is setup +self.workerMain = workerMain; // provide the "main" function that will be called by the privileged side when // lazy instantiating the compiler web worker -window.compilerMain = function compilerMain(): void { +self.compilerMain = function compilerMain(): void { // workerMain should have already been called since a compiler is a worker. - window.onmessage = async ({ + self.onmessage = async ({ data: request }: { data: CompilerRequest; }): Promise => { - const { rootNames, configPath, config } = request; - util.log(">>> compile start", { - rootNames, - type: CompilerRequestType[request.type] - }); - - // This will recursively analyse all the code for other imports, requesting - // those from the privileged side, populating the in memory cache which - // will be used by the host, before resolving. - const resolvedRootModules = ( - await processImports(rootNames.map(rootName => [rootName, rootName])) - ).map(info => info.url); - - const host = new Host( - request.type, - resolvedRootModules, - request.type === CompilerRequestType.Bundle ? request.outFile : undefined - ); - let emitSkipped = true; - let diagnostics: ts.Diagnostic[] | undefined; - - // if there is a configuration supplied, we need to parse that - if (config && config.length && configPath) { - const configResult = host.configure(configPath, config); - const ignoredOptions = configResult.ignoredOptions; - diagnostics = configResult.diagnostics; - if (ignoredOptions) { - console.warn( - yellow(`Unsupported compiler options in "${configPath}"\n`) + - cyan(` The following options were ignored:\n`) + - ` ${ignoredOptions - .map((value): string => bold(value)) - .join(", ")}` + switch (request.type) { + // `Compile` are requests from the internals to Deno, generated by both + // the `run` and `bundle` sub command. + case CompilerRequestType.Compile: { + const { bundle, config, configPath, outFile, rootNames } = request; + util.log(">>> compile start", { + rootNames, + type: CompilerRequestType[request.type] + }); + + // This will recursively analyse all the code for other imports, + // requesting those from the privileged side, populating the in memory + // cache which will be used by the host, before resolving. + const resolvedRootModules = await processImports( + rootNames.map(rootName => [rootName, rootName]) ); - } - } - // if there was a configuration and no diagnostics with it, we will continue - // to generate the program and possibly emit it. - if (!diagnostics || (diagnostics && diagnostics.length === 0)) { - const options = host.getCompilationSettings(); - const program = ts.createProgram(rootNames, options, host); - - diagnostics = ts - .getPreEmitDiagnostics(program) - .filter(({ code }): boolean => { - // TS1103: 'for-await-of' statement is only allowed within an async - // function or async generator. - if (code === 1103) return false; - // TS1308: 'await' expression is only allowed within an async - // function. - if (code === 1308) return false; - // TS2691: An import path cannot end with a '.ts' extension. Consider - // importing 'bad-module' instead. - if (code === 2691) return false; - // TS5009: Cannot find the common subdirectory path for the input files. - if (code === 5009) return false; - // TS5055: Cannot write file - // 'http://localhost:4545/tests/subdir/mt_application_x_javascript.j4.js' - // because it would overwrite input file. - if (code === 5055) return false; - // TypeScript is overly opinionated that only CommonJS modules kinds can - // support JSON imports. Allegedly this was fixed in - // Microsoft/TypeScript#26825 but that doesn't seem to be working here, - // so we will ignore complaints about this compiler setting. - if (code === 5070) return false; - return true; + // When a programme is emitted, TypeScript will call `writeFile` with + // each file that needs to be emitted. The Deno compiler host delegates + // this, to make it easier to perform the right actions, which vary + // based a lot on the request. For a `Compile` request, we need to + // cache all the files in the privileged side if we aren't bundling, + // and if we are bundling we need to enrich the bundle and either write + // out the bundle or log it to the console. + const state: WriteFileState = { + type: request.type, + bundle, + host: undefined, + outFile, + rootNames + }; + const writeFile = createWriteFile(state); + + const host = (state.host = new Host({ bundle, writeFile })); + let diagnostics: readonly ts.Diagnostic[] | undefined; + + // if there is a configuration supplied, we need to parse that + if (config && config.length && configPath) { + const configResult = host.configure(configPath, config); + diagnostics = processConfigureResponse(configResult, configPath); + } + + let emitSkipped = true; + // if there was a configuration and no diagnostics with it, we will continue + // to generate the program and possibly emit it. + if (!diagnostics || (diagnostics && diagnostics.length === 0)) { + const options = host.getCompilationSettings(); + const program = ts.createProgram(rootNames, options, host); + + diagnostics = ts + .getPreEmitDiagnostics(program) + .filter(({ code }) => !ignoredDiagnostics.includes(code)); + + // We will only proceed with the emit if there are no diagnostics. + if (diagnostics && diagnostics.length === 0) { + if (bundle) { + // we only support a single root module when bundling + assert(resolvedRootModules.length === 1); + // warning so it goes to stderr instead of stdout + console.warn(`Bundling "${resolvedRootModules[0]}"`); + setRootExports(program, resolvedRootModules[0]); + } + const emitResult = program.emit(); + emitSkipped = emitResult.emitSkipped; + // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned + // without casting. + diagnostics = emitResult.diagnostics; + } + } + + const result: CompileResult = { + emitSkipped, + diagnostics: diagnostics.length + ? fromTypeScriptDiagnostic(diagnostics) + : undefined + }; + postMessage(result); + + util.log("<<< compile end", { + rootNames, + type: CompilerRequestType[request.type] + }); + break; + } + case CompilerRequestType.RuntimeCompile: { + // `RuntimeCompile` are requests from a runtime user, both compiles and + // bundles. The process is similar to a request from the privileged + // side, but also returns the output to the on message. + const { rootName, sources, options, bundle } = request; + + util.log(">>> runtime compile start", { + rootName, + bundle, + sources: sources ? Object.keys(sources) : undefined }); - // We will only proceed with the emit if there are no diagnostics. - if (diagnostics && diagnostics.length === 0) { - if (request.type === CompilerRequestType.Bundle) { - // warning so it goes to stderr instead of stdout - console.warn(`Bundling "${resolvedRootModules.join(`", "`)}"`); + const resolvedRootName = sources + ? rootName + : resolveModules([rootName])[0]; + + const rootNames = sources + ? processLocalImports(sources, [[resolvedRootName, resolvedRootName]]) + : await processImports([[resolvedRootName, resolvedRootName]]); + + const state: WriteFileState = { + type: request.type, + bundle, + host: undefined, + rootNames, + sources, + emitMap: {}, + emitBundle: undefined + }; + const writeFile = createWriteFile(state); + + const host = (state.host = new Host({ bundle, writeFile })); + const compilerOptions = [defaultRuntimeCompileOptions]; + if (options) { + compilerOptions.push(convertCompilerOptions(options)); } - if (request.type === CompilerRequestType.Bundle) { - setRootExports(program, resolvedRootModules); + if (bundle) { + compilerOptions.push(defaultBundlerOptions); } + host.mergeOptions(...compilerOptions); + + const program = ts.createProgram( + rootNames, + host.getCompilationSettings(), + host + ); + + if (bundle) { + setRootExports(program, rootNames[0]); + } + + const diagnostics = ts + .getPreEmitDiagnostics(program) + .filter(({ code }) => !ignoredDiagnostics.includes(code)); + const emitResult = program.emit(); - emitSkipped = emitResult.emitSkipped; - // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned - // without casting. - diagnostics = emitResult.diagnostics as ts.Diagnostic[]; - } - } - const result: EmitResult = { - emitSkipped, - diagnostics: diagnostics.length - ? fromTypeScriptDiagnostic(diagnostics) - : undefined - }; + assert( + emitResult.emitSkipped === false, + "Unexpected skip of the emit." + ); + const { items } = fromTypeScriptDiagnostic(diagnostics); + const result = [ + items && items.length ? items : undefined, + bundle ? state.emitBundle : state.emitMap + ]; + postMessage(result); + + assert(state.emitMap); + util.log("<<< runtime compile finish", { + rootName, + sources: sources ? Object.keys(sources) : undefined, + bundle, + emitMap: Object.keys(state.emitMap) + }); - postMessage(result); + break; + } + case CompilerRequestType.RuntimeTranspile: { + const result: Record = {}; + const { sources, options } = request; + const compilerOptions = options + ? Object.assign( + {}, + defaultTranspileOptions, + convertCompilerOptions(options) + ) + : defaultTranspileOptions; + + for (const [fileName, inputText] of Object.entries(sources)) { + const { outputText: source, sourceMapText: map } = ts.transpileModule( + inputText, + { + fileName, + compilerOptions + } + ); + result[fileName] = { source, map }; + } + postMessage(result); - util.log("<<< compile end", { - rootNames, - type: CompilerRequestType[request.type] - }); + break; + } + default: + util.log( + `!!! unhandled CompilerRequestType: ${ + (request as CompilerRequest).type + } (${CompilerRequestType[(request as CompilerRequest).type]})` + ); + } // The compiler isolate exits after a single message. workerClose(); }; }; -function base64ToUint8Array(data: string): Uint8Array { - const binString = window.atob(data); - const size = binString.length; - const bytes = new Uint8Array(size); - for (let i = 0; i < size; i++) { - bytes[i] = binString.charCodeAt(i); - } - return bytes; -} - -window.wasmCompilerMain = function wasmCompilerMain(): void { +self.wasmCompilerMain = function wasmCompilerMain(): void { // workerMain should have already been called since a compiler is a worker. - window.onmessage = async ({ + self.onmessage = async ({ data: binary }: { data: string; }): Promise => { - const buffer = base64ToUint8Array(binary); + const buffer = util.base64ToUint8Array(binary); // @ts-ignore const compiled = await WebAssembly.compile(buffer); @@ -720,10 +310,7 @@ window.wasmCompilerMain = function wasmCompilerMain(): void { new Set(WebAssembly.Module.exports(compiled).map(({ name }) => name)) ); - postMessage({ - importList, - exportList - }); + postMessage({ importList, exportList }); util.log("<<< WASM compile end"); diff --git a/cli/js/compiler_api.ts b/cli/js/compiler_api.ts new file mode 100644 index 00000000000000..077d19bdd502c6 --- /dev/null +++ b/cli/js/compiler_api.ts @@ -0,0 +1,395 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +// This file contains the runtime APIs which will dispatch work to the internal +// compiler within Deno. + +import { Diagnostic } from "./diagnostics.ts"; +import * as dispatch from "./dispatch.ts"; +import { sendAsync } from "./dispatch_json.ts"; +import * as util from "./util.ts"; + +/** A specific subset TypeScript compiler options that can be supported by + * the Deno TypeScript compiler. */ +export interface CompilerOptions { + /** Allow JavaScript files to be compiled. Defaults to `true`. */ + allowJs?: boolean; + + /** Allow default imports from modules with no default export. This does not + * affect code emit, just typechecking. Defaults to `false`. */ + allowSyntheticDefaultImports?: boolean; + + /** Allow accessing UMD globals from modules. Defaults to `false`. */ + allowUmdGlobalAccess?: boolean; + + /** Do not report errors on unreachable code. Defaults to `false`. */ + allowUnreachableCode?: boolean; + + /** Do not report errors on unused labels. Defaults to `false` */ + allowUnusedLabels?: boolean; + + /** Parse in strict mode and emit `"use strict"` for each source file. + * Defaults to `true`. */ + alwaysStrict?: boolean; + + /** Base directory to resolve non-relative module names. Defaults to + * `undefined`. */ + baseUrl?: string; + + /** Report errors in `.js` files. Use in conjunction with `allowJs`. Defaults + * to `false`. */ + checkJs?: boolean; + + /** Generates corresponding `.d.ts` file. Defaults to `false`. */ + declaration?: boolean; + + /** Output directory for generated declaration files. */ + declarationDir?: string; + + /** Generates a source map for each corresponding `.d.ts` file. Defaults to + * `false`. */ + declarationMap?: boolean; + + /** Provide full support for iterables in `for..of`, spread and + * destructuring when targeting ES5 or ES3. Defaults to `false`. */ + downlevelIteration?: boolean; + + /** Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. + * Defaults to `false`. */ + emitBOM?: boolean; + + /** Only emit `.d.ts` declaration files. Defaults to `false`. */ + emitDeclarationOnly?: boolean; + + /** Emit design-type metadata for decorated declarations in source. See issue + * [microsoft/TypeScript#2577](https://github.com/Microsoft/TypeScript/issues/2577) + * for details. Defaults to `false`. */ + emitDecoratorMetadata?: boolean; + + /** Emit `__importStar` and `__importDefault` helpers for runtime babel + * ecosystem compatibility and enable `allowSyntheticDefaultImports` for type + * system compatibility. Defaults to `true`. */ + esModuleInterop?: boolean; + + /** Enables experimental support for ES decorators. Defaults to `false`. */ + experimentalDecorators?: boolean; + + /** Emit a single file with source maps instead of having a separate file. + * Defaults to `false`. */ + inlineSourceMap?: boolean; + + /** 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 `false`. */ + isolatedModules?: boolean; + + /** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`. + * Defaults to `"react"`. */ + jsx?: "react" | "preserve" | "react-native"; + + /** Specify the JSX factory function to use when targeting react JSX emit, + * e.g. `React.createElement` or `h`. Defaults to `React.createElement`. */ + jsxFactory?: string; + + /** Resolve keyof to string valued property names only (no numbers or + * symbols). Defaults to `false`. */ + keyofStringsOnly?: string; + + /** Emit class fields with ECMAScript-standard semantics. Defaults to `false`. + * Does not apply to `"esnext"` target. */ + useDefineForClassFields?: boolean; + + /** The locale to use to show error messages. */ + locale?: string; + + /** Specifies the location where debugger should locate map files instead of + * generated locations. Use this flag if the `.map` files will be located at + * run-time in a different location than the `.js` files. The location + * specified will be embedded in the source map to direct the debugger where + * the map files will be located. Defaults to `undefined`. */ + mapRoot?: string; + + /** Specify the module format for the emitted code. Defaults to + * `"esnext"`. */ + module?: + | "none" + | "commonjs" + | "amd" + | "system" + | "umd" + | "es6" + | "es2015" + | "esnext"; + + /** Do not generate custom helper functions like `__extends` in compiled + * output. Defaults to `false`. */ + noEmitHelpers?: boolean; + + /** Report errors for fallthrough cases in switch statement. Defaults to + * `false`. */ + noFallthroughCasesInSwitch?: boolean; + + /** Raise error on expressions and declarations with an implied any type. + * Defaults to `true`. */ + noImplicitAny?: boolean; + + /** Report an error when not all code paths in function return a value. + * Defaults to `false`. */ + noImplicitReturns?: boolean; + + /** Raise error on `this` expressions with an implied `any` type. Defaults to + * `true`. */ + noImplicitThis?: boolean; + + /** Do not emit `"use strict"` directives in module output. Defaults to + * `false`. */ + noImplicitUseStrict?: 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; + + /** Report errors on unused locals. Defaults to `false`. */ + noUnusedLocals?: boolean; + + /** Report errors on unused parameters. Defaults to `false`. */ + noUnusedParameters?: boolean; + + /** Redirect output structure to the directory. This only impacts + * `Deno.compile` and only changes the emitted file names. Defaults to + * `undefined`. */ + outDir?: string; + + /** List of path mapping entries for module names to locations relative to the + * `baseUrl`. Defaults to `undefined`. */ + paths?: Record; + + /** Do not erase const enum declarations in generated code. Defaults to + * `false`. */ + preserveConstEnums?: boolean; + + /** Remove all comments except copy-right header comments beginning with + * `/*!`. Defaults to `true`. */ + removeComments?: boolean; + + /** Include modules imported with `.json` extension. Defaults to `true`. */ + resolveJsonModule?: boolean; + + /** Specifies the root directory of input files. Only use to control the + * output directory structure with `outDir`. Defaults to `undefined`. */ + rootDir?: string; + + /** List of _root_ folders whose combined content represent the structure of + * the project at runtime. Defaults to `undefined`. */ + rootDirs?: string[]; + + /** Generates corresponding `.map` file. Defaults to `false`. */ + sourceMap?: boolean; + + /** Specifies the location where debugger should locate TypeScript files + * instead of source locations. Use this flag if the sources will be located + * at run-time in a different location than that at design-time. The location + * specified will be embedded in the sourceMap to direct the debugger where + * the source files will be located. Defaults to `undefined`. */ + sourceRoot?: string; + + /** Enable all strict type checking options. Enabling `strict` enables + * `noImplicitAny`, `noImplicitThis`, `alwaysStrict`, `strictBindCallApply`, + * `strictNullChecks`, `strictFunctionTypes` and + * `strictPropertyInitialization`. Defaults to `true`. */ + strict?: boolean; + + /** Enable stricter checking of the `bind`, `call`, and `apply` methods on + * functions. Defaults to `true`. */ + strictBindCallApply?: boolean; + + /** Disable bivariant parameter checking for function types. Defaults to + * `true`. */ + strictFunctionTypes?: boolean; + + /** Ensure non-undefined class properties are initialized in the constructor. + * This option requires `strictNullChecks` be enabled in order to take effect. + * Defaults to `true`. */ + strictPropertyInitialization?: boolean; + + /** In strict null checking mode, the `null` and `undefined` values are not in + * the domain of every type and are only assignable to themselves and `any` + * (the one exception being that `undefined` is also assignable to `void`). */ + strictNullChecks?: boolean; + + /** Suppress excess property checks for object literals. Defaults to + * `false`. */ + suppressExcessPropertyErrors?: boolean; + + /** Suppress `noImplicitAny` errors for indexing objects lacking index + * signatures. */ + suppressImplicitAnyIndexErrors?: boolean; + + /** Specify ECMAScript target version. Defaults to `esnext`. */ + target?: + | "es3" + | "es5" + | "es6" + | "es2015" + | "es2016" + | "es2017" + | "es2018" + | "es2019" + | "es2020" + | "esnext"; + + /** List of names of type definitions to include. Defaults to `undefined`. */ + types?: string[]; +} + +/** Internal function to just validate that the specifier looks relative, that + * it starts with `./`. */ +function checkRelative(specifier: string): string { + return specifier.match(/^([\.\/\\]|https?:\/{2}|file:\/{2})/) + ? specifier + : `./${specifier}`; +} + +/** 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; +} + +/** Takes a set of TypeScript sources and resolves with 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. + * + * 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. + * Many of the options related to type checking and emitting + * type declaration files will have no impact on the output. + */ +export function transpileOnly( + sources: Record, + options?: CompilerOptions +): Promise> { + util.log("Deno.transpileOnly", { sources: Object.keys(sources), options }); + const payload = { + sources, + options: options ? JSON.stringify(options) : undefined + }; + return sendAsync(dispatch.OP_TRANSPILE, payload).then(result => + JSON.parse(result) + ); +} + +/** Takes a root module name, any optionally a record set of sources. Resolves + * with a compiled set of modules. 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. + * + * 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]> { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: options ? JSON.stringify(options) : undefined, + bundle: false + }; + util.log("Deno.compile", { + rootName: payload.rootName, + sources: !!sources, + options + }); + return sendAsync(dispatch.OP_COMPILE, payload).then(result => + JSON.parse(result) + ); +} + +/** Takes a root module name, and optionally a record set of sources. Resolves + * with a single JavaScript string 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. + * + * 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. + */ +export function bundle( + rootName: string, + sources?: Record, + options?: CompilerOptions +): Promise<[Diagnostic[] | undefined, string]> { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: options ? JSON.stringify(options) : undefined, + bundle: true + }; + util.log("Deno.bundle", { + rootName: payload.rootName, + sources: !!sources, + options + }); + return sendAsync(dispatch.OP_COMPILE, payload).then(result => + JSON.parse(result) + ); +} diff --git a/cli/js/compiler_api_test.ts b/cli/js/compiler_api_test.ts new file mode 100644 index 00000000000000..802fa6d4630213 --- /dev/null +++ b/cli/js/compiler_api_test.ts @@ -0,0 +1,105 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals, test } from "./test_util.ts"; + +const { compile, transpileOnly, bundle } = Deno; + +test(async function compilerApiCompileSources() { + const [diagnostics, actual] = await 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); + assertEquals(Object.keys(actual), [ + "/bar.js.map", + "/bar.js", + "/foo.js.map", + "/foo.js" + ]); +}); + +test(async function compilerApiCompileNoSources() { + const [diagnostics, actual] = await compile("./cli/tests/subdir/mod1.ts"); + assert(diagnostics == null); + assert(actual); + const keys = Object.keys(actual); + assertEquals(keys.length, 6); + assert(keys[0].endsWith("print_hello.js.map")); + assert(keys[1].endsWith("print_hello.js")); +}); + +test(async function compilerApiCompileOptions() { + const [diagnostics, actual] = await compile( + "/foo.ts", + { + "/foo.ts": `export const foo = "foo";` + }, + { + module: "amd", + sourceMap: false + } + ); + assert(diagnostics == null); + assert(actual); + assertEquals(Object.keys(actual), ["/foo.js"]); + assert(actual["/foo.js"].startsWith("define(")); +}); + +test(async function transpileOnlyApi() { + const actual = await 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); +}); + +test(async function transpileOnlyApiConfig() { + const actual = await transpileOnly( + { + "foo.ts": `export enum Foo { Foo, Bar, Baz };\n` + }, + { + sourceMap: false, + module: "amd" + } + ); + assert(actual); + assertEquals(Object.keys(actual), ["foo.ts"]); + assert(actual["foo.ts"].source.startsWith("define(")); + assert(actual["foo.ts"].map == null); +}); + +test(async function bundleApiSources() { + const [diagnostics, actual] = await bundle("/foo.ts", { + "/foo.ts": `export * from "./bar.ts";\n`, + "/bar.ts": `export const bar = "bar";\n` + }); + assert(diagnostics == null); + assert(actual.includes(`instantiate("foo")`)); + assert(actual.includes(`__rootExports["bar"]`)); +}); + +test(async function bundleApiNoSources() { + const [diagnostics, actual] = await bundle("./cli/tests/subdir/mod1.ts"); + assert(diagnostics == null); + assert(actual.includes(`instantiate("mod1")`)); + assert(actual.includes(`__rootExports["printHello3"]`)); +}); + +test(async function bundleApiConfig() { + const [diagnostics, actual] = await bundle( + "/foo.ts", + { + "/foo.ts": `// random comment\nexport * from "./bar.ts";\n`, + "/bar.ts": `export const bar = "bar";\n` + }, + { + removeComments: true + } + ); + assert(diagnostics == null); + assert(!actual.includes(`random`)); +}); diff --git a/cli/js/bundler.ts b/cli/js/compiler_bundler.ts similarity index 67% rename from cli/js/bundler.ts rename to cli/js/compiler_bundler.ts index 637c11ffcf11ba..a4e4557ca62504 100644 --- a/cli/js/bundler.ts +++ b/cli/js/compiler_bundler.ts @@ -1,41 +1,47 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { Console } from "./console.ts"; import * as dispatch from "./dispatch.ts"; import { sendSync } from "./dispatch_json.ts"; -import { TextEncoder } from "./text_encoding.ts"; -import { assert, commonPath, humanFileSize } from "./util.ts"; -import { writeFileSync } from "./write_file.ts"; - -declare global { - const console: Console; -} +import { + assert, + commonPath, + normalizeString, + CHAR_FORWARD_SLASH +} from "./util.ts"; const BUNDLE_LOADER = "bundle_loader.js"; -const encoder = new TextEncoder(); - +/** A loader of bundled modules that we will inline into any bundle outputs. */ let bundleLoader: string; +/** Local state of what the root exports are of a root module. */ let rootExports: string[] | undefined; -/** Given a fileName and the data, emit the file to the file system. */ -export function emitBundle( - rootNames: string[], - fileName: string | undefined, +/** Take a URL and normalize it, resolving relative path parts. */ +function normalizeUrl(rootName: string): string { + const match = /^(\S+:\/{2,3})(.+)$/.exec(rootName); + if (match) { + const [, protocol, path] = match; + return `${protocol}${normalizeString( + path, + false, + "/", + code => code === CHAR_FORWARD_SLASH + )}`; + } else { + return rootName; + } +} + +/** Given a root name, contents, and source files, enrich the data of the + * bundle with a loader and re-export the exports of the root name. */ +export function buildBundle( + rootName: string, data: string, sourceFiles: readonly ts.SourceFile[] -): void { - // if the fileName is set to an internal value, just noop - if (fileName && fileName.startsWith("$deno$")) { - return; - } - // This should never happen at the moment, but this code can't currently - // support it - assert( - rootNames.length === 1, - "Only single root modules supported for bundling." - ); +): string { + // we can only do this once we are bootstrapped and easiest way to do it is + // inline here if (!bundleLoader) { bundleLoader = sendSync(dispatch.OP_FETCH_ASSET, { name: BUNDLE_LOADER }); } @@ -45,7 +51,9 @@ export function emitBundle( // publicly, so we have to try to replicate const sources = sourceFiles.map(sf => sf.fileName); const sharedPath = commonPath(sources); - const rootName = rootNames[0].replace(sharedPath, "").replace(/\.\w+$/i, ""); + rootName = normalizeUrl(rootName) + .replace(sharedPath, "") + .replace(/\.\w+$/i, ""); let instantiate: string; if (rootExports && rootExports.length) { instantiate = `const __rootExports = instantiate("${rootName}");\n`; @@ -59,28 +67,16 @@ export function emitBundle( } else { instantiate = `instantiate("${rootName}");\n`; } - const bundle = `${bundleLoader}\n${data}\n${instantiate}`; - if (fileName) { - const encodedData = encoder.encode(bundle); - console.warn(`Emitting bundle to "${fileName}"`); - writeFileSync(fileName, encodedData); - console.warn(`${humanFileSize(encodedData.length)} emitted.`); - } else { - console.log(bundle); - } + return `${bundleLoader}\n${data}\n${instantiate}`; } /** Set the rootExports which will by the `emitBundle()` */ -export function setRootExports( - program: ts.Program, - rootModules: string[] -): void { +export function setRootExports(program: ts.Program, rootModule: string): void { // get a reference to the type checker, this will let us find symbols from // the AST. const checker = program.getTypeChecker(); - assert(rootModules.length === 1); // get a reference to the main source file for the bundle - const mainSourceFile = program.getSourceFile(rootModules[0]); + const mainSourceFile = program.getSourceFile(rootModule); assert(mainSourceFile); // retrieve the internal TypeScript symbol for this AST node const mainSymbol = checker.getSymbolAtLocation(mainSourceFile); diff --git a/cli/js/compiler_host.ts b/cli/js/compiler_host.ts new file mode 100644 index 00000000000000..89f5f506dd4e27 --- /dev/null +++ b/cli/js/compiler_host.ts @@ -0,0 +1,302 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { MediaType, SourceFile } from "./compiler_sourcefile.ts"; +import { OUT_DIR, WriteFileCallback } from "./compiler_util.ts"; +import { cwd } from "./dir.ts"; +import { sendSync } from "./dispatch_json.ts"; +import * as dispatch from "./dispatch.ts"; +import { assert, notImplemented } from "./util.ts"; +import * as util from "./util.ts"; + +export interface CompilerHostOptions { + bundle?: boolean; + writeFile: WriteFileCallback; +} + +export interface ConfigureResponse { + ignoredOptions?: string[]; + diagnostics?: ts.Diagnostic[]; +} + +const ASSETS = "$asset$"; + +/** Options that need to be used when generating a bundle (either trusted or + * runtime). */ +export const defaultBundlerOptions: ts.CompilerOptions = { + inlineSourceMap: false, + module: ts.ModuleKind.AMD, + outDir: undefined, + outFile: `${OUT_DIR}/bundle.js`, + // disabled until we have effective way to modify source maps + sourceMap: false +}; + +/** Default options used by the compiler Host when compiling. */ +export const defaultCompileOptions: ts.CompilerOptions = { + allowJs: true, + allowNonTsExtensions: true, + // TODO(#3324) Enable strict mode for user code. + // strict: true, + checkJs: false, + esModuleInterop: true, + module: ts.ModuleKind.ESNext, + outDir: OUT_DIR, + resolveJsonModule: true, + sourceMap: true, + stripComments: true, + target: ts.ScriptTarget.ESNext, + jsx: ts.JsxEmit.React +}; + +/** Options that need to be used when doing a runtime (non bundled) compilation */ +export const defaultRuntimeCompileOptions: ts.CompilerOptions = { + outDir: undefined +}; + +/** Default options used when doing a transpile only. */ +export const defaultTranspileOptions: ts.CompilerOptions = { + esModuleInterop: true, + module: ts.ModuleKind.ESNext, + sourceMap: true, + scriptComments: true, + target: ts.ScriptTarget.ESNext +}; + +/** Options that either do nothing in Deno, or would cause undesired behavior + * if modified. */ +const ignoredCompilerOptions: readonly string[] = [ + "allowSyntheticDefaultImports", + "baseUrl", + "build", + "composite", + "declaration", + "declarationDir", + "declarationMap", + "diagnostics", + "downlevelIteration", + "emitBOM", + "emitDeclarationOnly", + "esModuleInterop", + "extendedDiagnostics", + "forceConsistentCasingInFileNames", + "help", + "importHelpers", + "incremental", + "inlineSourceMap", + "inlineSources", + "init", + "isolatedModules", + "lib", + "listEmittedFiles", + "listFiles", + "mapRoot", + "maxNodeModuleJsDepth", + "module", + "moduleResolution", + "newLine", + "noEmit", + "noEmitHelpers", + "noEmitOnError", + "noLib", + "noResolve", + "out", + "outDir", + "outFile", + "paths", + "preserveSymlinks", + "preserveWatchOutput", + "pretty", + "rootDir", + "rootDirs", + "showConfig", + "skipDefaultLibCheck", + "skipLibCheck", + "sourceMap", + "sourceRoot", + "stripInternal", + "target", + "traceResolution", + "tsBuildInfoFile", + "types", + "typeRoots", + "version", + "watch" +]; + +export class Host implements ts.CompilerHost { + private readonly _options = defaultCompileOptions; + + private _writeFile: WriteFileCallback; + + private _getAsset(filename: string): SourceFile { + const sourceFile = SourceFile.get(filename); + if (sourceFile) { + return sourceFile; + } + const url = filename.split("/").pop()!; + const name = url.includes(".") ? url : `${url}.d.ts`; + const sourceCode = sendSync(dispatch.OP_FETCH_ASSET, { name }); + return new SourceFile({ + url, + filename, + mediaType: MediaType.TypeScript, + sourceCode + }); + } + + /* Deno specific APIs */ + + /** Provides the `ts.HostCompiler` interface for Deno. */ + constructor(options: CompilerHostOptions) { + const { bundle = false, writeFile } = options; + this._writeFile = writeFile; + if (bundle) { + // options we need to change when we are generating a bundle + Object.assign(this._options, defaultBundlerOptions); + } + } + + /** Take a configuration string, parse it, and use it to merge with the + * compiler's configuration options. The method returns an array of compiler + * options which were ignored, or `undefined`. */ + configure(path: string, configurationText: string): ConfigureResponse { + util.log("compiler::host.configure", path); + assert(configurationText); + const { config, error } = ts.parseConfigFileTextToJson( + path, + configurationText + ); + if (error) { + return { diagnostics: [error] }; + } + const { options, errors } = ts.convertCompilerOptionsFromJson( + config.compilerOptions, + cwd() + ); + const ignoredOptions: string[] = []; + for (const key of Object.keys(options)) { + if ( + ignoredCompilerOptions.includes(key) && + (!(key in this._options) || options[key] !== this._options[key]) + ) { + ignoredOptions.push(key); + delete options[key]; + } + } + Object.assign(this._options, options); + return { + ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined, + diagnostics: errors.length ? errors : undefined + }; + } + + /** Merge options into the host's current set of compiler options and return + * the merged set. */ + mergeOptions(...options: ts.CompilerOptions[]): ts.CompilerOptions { + Object.assign(this._options, ...options); + return Object.assign({}, this._options); + } + + /* TypeScript CompilerHost APIs */ + + fileExists(_fileName: string): boolean { + return notImplemented(); + } + + getCanonicalFileName(fileName: string): string { + return fileName; + } + + getCompilationSettings(): ts.CompilerOptions { + util.log("compiler::host.getCompilationSettings()"); + return this._options; + } + + getCurrentDirectory(): string { + return ""; + } + + getDefaultLibFileName(_options: ts.CompilerOptions): string { + return ASSETS + "/lib.deno_runtime.d.ts"; + } + + getNewLine(): string { + return "\n"; + } + + getSourceFile( + fileName: string, + languageVersion: ts.ScriptTarget, + onError?: (message: string) => void, + shouldCreateNewSourceFile?: boolean + ): ts.SourceFile | undefined { + util.log("compiler::host.getSourceFile", fileName); + try { + assert(!shouldCreateNewSourceFile); + const sourceFile = fileName.startsWith(ASSETS) + ? this._getAsset(fileName) + : SourceFile.get(fileName); + assert(sourceFile != null); + if (!sourceFile.tsSourceFile) { + sourceFile.tsSourceFile = ts.createSourceFile( + fileName, + sourceFile.sourceCode, + languageVersion + ); + } + return sourceFile!.tsSourceFile; + } catch (e) { + if (onError) { + onError(String(e)); + } else { + throw e; + } + return undefined; + } + } + + readFile(_fileName: string): string | undefined { + return notImplemented(); + } + + resolveModuleNames( + moduleNames: string[], + containingFile: string + ): Array { + util.log("compiler::host.resolveModuleNames", { + moduleNames, + containingFile + }); + return moduleNames.map(specifier => { + const url = SourceFile.getUrl(specifier, containingFile); + const sourceFile = specifier.startsWith(ASSETS) + ? this._getAsset(specifier) + : url + ? SourceFile.get(url) + : undefined; + if (!sourceFile) { + return undefined; + } + return { + resolvedFileName: sourceFile.url, + isExternalLibraryImport: specifier.startsWith(ASSETS), + extension: sourceFile.extension + }; + }); + } + + useCaseSensitiveFileNames(): boolean { + return true; + } + + writeFile( + fileName: string, + data: string, + _writeByteOrderMark: boolean, + _onError?: (message: string) => void, + sourceFiles?: readonly ts.SourceFile[] + ): void { + util.log("compiler::host.writeFile", fileName); + this._writeFile(fileName, data, sourceFiles); + } +} diff --git a/cli/js/compiler_imports.ts b/cli/js/compiler_imports.ts new file mode 100644 index 00000000000000..d861f8ddc8de93 --- /dev/null +++ b/cli/js/compiler_imports.ts @@ -0,0 +1,179 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { + MediaType, + SourceFile, + SourceFileJson +} from "./compiler_sourcefile.ts"; +import { cwd } from "./dir.ts"; +import * as dispatch from "./dispatch.ts"; +import { sendAsync, sendSync } from "./dispatch_json.ts"; +import { assert } from "./util.ts"; +import * as util from "./util.ts"; + +/** Resolve a path to the final path segment passed. */ +function resolvePath(...pathSegments: string[]): string { + let resolvedPath = ""; + let resolvedAbsolute = false; + + for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + let path: string; + + if (i >= 0) path = pathSegments[i]; + else path = cwd(); + + // Skip empty entries + if (path.length === 0) { + continue; + } + + resolvedPath = `${path}/${resolvedPath}`; + resolvedAbsolute = path.charCodeAt(0) === util.CHAR_FORWARD_SLASH; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when cwd() fails) + + // Normalize the path + resolvedPath = util.normalizeString( + resolvedPath, + !resolvedAbsolute, + "/", + code => code === util.CHAR_FORWARD_SLASH + ); + + if (resolvedAbsolute) { + if (resolvedPath.length > 0) return `/${resolvedPath}`; + else return "/"; + } else if (resolvedPath.length > 0) return resolvedPath; + else return "."; +} + +/** Resolve a relative specifier based on the referrer. Used when resolving + * modules internally within the runtime compiler API. */ +function resolveSpecifier(specifier: string, referrer: string): string { + if (!specifier.startsWith(".")) { + return specifier; + } + const pathParts = referrer.split("/"); + pathParts.pop(); + let path = pathParts.join("/"); + path = path.endsWith("/") ? path : `${path}/`; + return resolvePath(path, specifier); +} + +/** Ops to Rust to resolve modules' URLs. */ +export function resolveModules( + specifiers: string[], + referrer?: string +): string[] { + util.log("compiler_imports::resolveModules", { specifiers, referrer }); + return sendSync(dispatch.OP_RESOLVE_MODULES, { specifiers, referrer }); +} + +/** Ops to Rust to fetch modules meta data. */ +function fetchSourceFiles( + specifiers: string[], + referrer?: string +): Promise { + util.log("compiler_imports::fetchSourceFiles", { specifiers, referrer }); + return sendAsync(dispatch.OP_FETCH_SOURCE_FILES, { + specifiers, + referrer + }); +} + +/** Given a filename, determine the media type based on extension. Used when + * resolving modules internally in a runtime compile. */ +function getMediaType(filename: string): MediaType { + const maybeExtension = /\.([a-zA-Z]+)$/.exec(filename); + if (!maybeExtension) { + util.log(`!!! Could not identify valid extension: "${filename}"`); + return MediaType.Unknown; + } + const [, extension] = maybeExtension; + switch (extension.toLowerCase()) { + case "js": + return MediaType.JavaScript; + case "jsx": + return MediaType.JSX; + case "json": + return MediaType.Json; + case "ts": + return MediaType.TypeScript; + case "tsx": + return MediaType.TSX; + case "wasm": + return MediaType.Wasm; + default: + util.log(`!!! Unknown extension: "${extension}"`); + return MediaType.Unknown; + } +} + +/** Recursively process the imports of modules from within the supplied sources, + * generating `SourceFile`s of any imported files. + * + * Specifiers are supplied in an array of tuples where the first is the + * specifier that will be requested in the code and the second is the specifier + * that should be actually resolved. */ +export function processLocalImports( + sources: Record, + specifiers: Array<[string, string]>, + referrer?: string +): string[] { + if (!specifiers.length) { + return []; + } + const moduleNames = specifiers.map( + referrer + ? ([, specifier]): string => resolveSpecifier(specifier, referrer) + : ([, specifier]): string => specifier + ); + for (let i = 0; i < moduleNames.length; i++) { + const moduleName = moduleNames[i]; + assert(moduleName in sources, `Missing module in sources: "${moduleName}"`); + const sourceFile = + SourceFile.get(moduleName) || + new SourceFile({ + url: moduleName, + filename: moduleName, + sourceCode: sources[moduleName], + mediaType: getMediaType(moduleName) + }); + sourceFile.cache(specifiers[i][0], referrer); + if (!sourceFile.processed) { + processLocalImports(sources, sourceFile.imports(), sourceFile.url); + } + } + return moduleNames; +} + +/** Recursively process the imports of modules, generating `SourceFile`s of any + * imported files. + * + * Specifiers are supplied in an array of tuples where the first is the + * specifier that will be requested in the code and the second is the specifier + * that should be actually resolved. */ +export async function processImports( + specifiers: Array<[string, string]>, + referrer?: string +): Promise { + if (!specifiers.length) { + return []; + } + const sources = specifiers.map(([, moduleSpecifier]) => moduleSpecifier); + const resolvedSources = resolveModules(sources, referrer); + const sourceFiles = await fetchSourceFiles(resolvedSources, referrer); + assert(sourceFiles.length === specifiers.length); + for (let i = 0; i < sourceFiles.length; i++) { + const sourceFileJson = sourceFiles[i]; + const sourceFile = + SourceFile.get(sourceFileJson.url) || new SourceFile(sourceFileJson); + sourceFile.cache(specifiers[i][0], referrer); + if (!sourceFile.processed) { + await processImports(sourceFile.imports(), sourceFile.url); + } + } + return resolvedSources; +} diff --git a/cli/js/compiler_sourcefile.ts b/cli/js/compiler_sourcefile.ts new file mode 100644 index 00000000000000..46e5cbe3b252b9 --- /dev/null +++ b/cli/js/compiler_sourcefile.ts @@ -0,0 +1,168 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { + getMappedModuleName, + parseTypeDirectives +} from "./compiler_type_directives.ts"; +import { assert, log } from "./util.ts"; + +// Warning! The values in this enum are duplicated in `cli/msg.rs` +// Update carefully! +export enum MediaType { + JavaScript = 0, + JSX = 1, + TypeScript = 2, + TSX = 3, + Json = 4, + Wasm = 5, + Unknown = 6 +} + +/** The shape of the SourceFile that comes from the privileged side */ +export interface SourceFileJson { + url: string; + filename: string; + mediaType: MediaType; + sourceCode: string; +} + +/** Returns the TypeScript Extension enum for a given media type. */ +function getExtension(fileName: string, mediaType: MediaType): ts.Extension { + switch (mediaType) { + case MediaType.JavaScript: + return ts.Extension.Js; + case MediaType.JSX: + return ts.Extension.Jsx; + case MediaType.TypeScript: + return fileName.endsWith(".d.ts") ? ts.Extension.Dts : ts.Extension.Ts; + case MediaType.TSX: + return ts.Extension.Tsx; + case MediaType.Json: + return ts.Extension.Json; + case MediaType.Wasm: + // Custom marker for Wasm type. + return ts.Extension.Js; + case MediaType.Unknown: + default: + throw TypeError("Cannot resolve extension."); + } +} + +/** A self registering abstraction of source files. */ +export class SourceFile { + extension!: ts.Extension; + filename!: string; + + /** An array of tuples which represent the imports for the source file. The + * first element is the one that will be requested at compile time, the + * second is the one that should be actually resolved. This provides the + * feature of type directives for Deno. */ + importedFiles?: Array<[string, string]>; + + mediaType!: MediaType; + processed = false; + sourceCode!: string; + tsSourceFile?: ts.SourceFile; + url!: string; + + constructor(json: SourceFileJson) { + if (SourceFile._moduleCache.has(json.url)) { + throw new TypeError("SourceFile already exists"); + } + Object.assign(this, json); + this.extension = getExtension(this.url, this.mediaType); + SourceFile._moduleCache.set(this.url, this); + } + + /** Cache the source file to be able to be retrieved by `moduleSpecifier` and + * `containingFile`. */ + cache(moduleSpecifier: string, containingFile?: string): void { + containingFile = containingFile || ""; + let innerCache = SourceFile._specifierCache.get(containingFile); + if (!innerCache) { + innerCache = new Map(); + SourceFile._specifierCache.set(containingFile, innerCache); + } + innerCache.set(moduleSpecifier, this); + } + + /** Process the imports for the file and return them. */ + imports(): Array<[string, string]> { + if (this.processed) { + throw new Error("SourceFile has already been processed."); + } + assert(this.sourceCode != null); + // we shouldn't process imports for files which contain the nocheck pragma + // (like bundles) + if (this.sourceCode.match(/\/{2}\s+@ts-nocheck/)) { + log(`Skipping imports for "${this.filename}"`); + return []; + } + const preProcessedFileInfo = ts.preProcessFile(this.sourceCode, true, true); + this.processed = true; + const files = (this.importedFiles = [] as Array<[string, string]>); + + function process(references: ts.FileReference[]): void { + for (const { fileName } of references) { + files.push([fileName, fileName]); + } + } + + const { + importedFiles, + referencedFiles, + libReferenceDirectives, + typeReferenceDirectives + } = preProcessedFileInfo; + const typeDirectives = parseTypeDirectives(this.sourceCode); + if (typeDirectives) { + for (const importedFile of importedFiles) { + files.push([ + importedFile.fileName, + getMappedModuleName(importedFile, typeDirectives) + ]); + } + } else { + process(importedFiles); + } + process(referencedFiles); + process(libReferenceDirectives); + process(typeReferenceDirectives); + return files; + } + + /** A cache of all the source files which have been loaded indexed by the + * url. */ + private static _moduleCache: Map = new Map(); + + /** A cache of source files based on module specifiers and containing files + * which is used by the TypeScript compiler to resolve the url */ + private static _specifierCache: Map< + string, + Map + > = new Map(); + + /** Retrieve a `SourceFile` based on a `moduleSpecifier` and `containingFile` + * or return `undefined` if not preset. */ + static getUrl( + moduleSpecifier: string, + containingFile: string + ): string | undefined { + const containingCache = this._specifierCache.get(containingFile); + if (containingCache) { + const sourceFile = containingCache.get(moduleSpecifier); + return sourceFile && sourceFile.url; + } + return undefined; + } + + /** Retrieve a `SourceFile` based on a `url` */ + static get(url: string): SourceFile | undefined { + return this._moduleCache.get(url); + } + + /** Determine if a source file exists or not */ + static has(url: string): boolean { + return this._moduleCache.has(url); + } +} diff --git a/cli/js/type_directives.ts b/cli/js/compiler_type_directives.ts similarity index 100% rename from cli/js/type_directives.ts rename to cli/js/compiler_type_directives.ts diff --git a/cli/js/compiler_util.ts b/cli/js/compiler_util.ts new file mode 100644 index 00000000000000..30c6f61629fcff --- /dev/null +++ b/cli/js/compiler_util.ts @@ -0,0 +1,298 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { bold, cyan, yellow } from "./colors.ts"; +import { CompilerOptions } from "./compiler_api.ts"; +import { buildBundle } from "./compiler_bundler.ts"; +import { ConfigureResponse, Host } from "./compiler_host.ts"; +import { SourceFile } from "./compiler_sourcefile.ts"; +import { sendSync } from "./dispatch_json.ts"; +import * as dispatch from "./dispatch.ts"; +import { TextEncoder } from "./text_encoding.ts"; +import * as util from "./util.ts"; +import { assert } from "./util.ts"; +import { writeFileSync } from "./write_file.ts"; + +/** Type for the write fall callback that allows delegation from the compiler + * host on writing files. */ +export type WriteFileCallback = ( + fileName: string, + data: string, + sourceFiles?: readonly ts.SourceFile[] +) => void; + +/** An object which is passed to `createWriteFile` to be used to read and set + * state related to the emit of a program. */ +export interface WriteFileState { + type: CompilerRequestType; + bundle?: boolean; + host?: Host; + outFile?: string; + rootNames: string[]; + emitMap?: Record; + emitBundle?: string; + sources?: Record; +} + +// Warning! The values in this enum are duplicated in `cli/msg.rs` +// Update carefully! +export enum CompilerRequestType { + Compile = 0, + RuntimeCompile = 1, + RuntimeTranspile = 2 +} + +export const OUT_DIR = "$deno$"; + +/** Cache the contents of a file on the trusted side. */ +function cache( + moduleId: string, + emittedFileName: string, + contents: string, + checkJs = false +): void { + util.log("compiler::cache", { moduleId, emittedFileName, checkJs }); + const sf = SourceFile.get(moduleId); + + if (sf) { + // NOTE: If it's a `.json` file we don't want to write it to disk. + // JSON files are loaded and used by TS compiler to check types, but we don't want + // to emit them to disk because output file is the same as input file. + if (sf.extension === ts.Extension.Json) { + return; + } + + // NOTE: JavaScript files are only cached to disk if `checkJs` + // option in on + if (sf.extension === ts.Extension.Js && !checkJs) { + return; + } + } + + if (emittedFileName.endsWith(".map")) { + // Source Map + sendSync(dispatch.OP_CACHE, { + extension: ".map", + moduleId, + contents + }); + } else if ( + emittedFileName.endsWith(".js") || + emittedFileName.endsWith(".json") + ) { + // Compiled JavaScript + sendSync(dispatch.OP_CACHE, { + extension: ".js", + moduleId, + contents + }); + } else { + assert(false, `Trying to cache unhandled file type "${emittedFileName}"`); + } +} + +const encoder = new TextEncoder(); + +/** Generates a `writeFile` function which can be passed to the compiler `Host` + * to use when emitting files. */ +export function createWriteFile(state: WriteFileState): WriteFileCallback { + if (state.type === CompilerRequestType.Compile) { + return function writeFile( + fileName: string, + data: string, + sourceFiles?: readonly ts.SourceFile[] + ): void { + assert( + sourceFiles != null, + `Unexpected emit of "${fileName}" which isn't part of a program.` + ); + assert(state.host); + if (!state.bundle) { + assert(sourceFiles.length === 1); + cache( + sourceFiles[0].fileName, + fileName, + data, + state.host.getCompilationSettings().checkJs + ); + } else { + // if the fileName is set to an internal value, just noop, this is + // used in the Rust unit tests. + if (state.outFile && state.outFile.startsWith(OUT_DIR)) { + return; + } + // we only support single root names for bundles + assert( + state.rootNames.length === 1, + `Only one root name supported. Got "${JSON.stringify( + state.rootNames + )}"` + ); + // this enriches the string with the loader and re-exports the + // exports of the root module + const content = buildBundle(state.rootNames[0], data, sourceFiles); + if (state.outFile) { + const encodedData = encoder.encode(content); + console.warn(`Emitting bundle to "${state.outFile}"`); + writeFileSync(state.outFile, encodedData); + console.warn(`${util.humanFileSize(encodedData.length)} emitted.`); + } else { + console.log(content); + } + } + }; + } + + return function writeFile( + fileName: string, + data: string, + sourceFiles?: readonly ts.SourceFile[] + ): void { + assert(sourceFiles != null); + assert(state.host); + assert(state.emitMap); + if (!state.bundle) { + assert(sourceFiles.length === 1); + state.emitMap[fileName] = data; + // we only want to cache the compiler output if we are resolving + // modules externally + if (!state.sources) { + cache( + sourceFiles[0].fileName, + fileName, + data, + state.host.getCompilationSettings().checkJs + ); + } + } else { + // we only support single root names for bundles + assert(state.rootNames.length === 1); + state.emitBundle = buildBundle(state.rootNames[0], data, sourceFiles); + } + }; +} + +/** Take a runtime set of compiler options as stringified JSON and convert it + * to a set of TypeScript compiler options. */ +export function convertCompilerOptions(str: string): ts.CompilerOptions { + const options: CompilerOptions = JSON.parse(str); + const out: Record = {}; + const keys = Object.keys(options) as Array; + for (const key of keys) { + switch (key) { + case "jsx": + const value = options[key]; + if (value === "preserve") { + out[key] = ts.JsxEmit.Preserve; + } else if (value === "react") { + out[key] = ts.JsxEmit.React; + } else { + out[key] = ts.JsxEmit.ReactNative; + } + break; + case "module": + switch (options[key]) { + case "amd": + out[key] = ts.ModuleKind.AMD; + break; + case "commonjs": + out[key] = ts.ModuleKind.CommonJS; + break; + case "es2015": + case "es6": + out[key] = ts.ModuleKind.ES2015; + break; + case "esnext": + out[key] = ts.ModuleKind.ESNext; + break; + case "none": + out[key] = ts.ModuleKind.None; + break; + case "system": + out[key] = ts.ModuleKind.System; + break; + case "umd": + out[key] = ts.ModuleKind.UMD; + break; + default: + throw new TypeError("Unexpected module type"); + } + break; + case "target": + switch (options[key]) { + case "es3": + out[key] = ts.ScriptTarget.ES3; + break; + case "es5": + out[key] = ts.ScriptTarget.ES5; + break; + case "es6": + case "es2015": + out[key] = ts.ScriptTarget.ES2015; + break; + case "es2016": + out[key] = ts.ScriptTarget.ES2016; + break; + case "es2017": + out[key] = ts.ScriptTarget.ES2017; + break; + case "es2018": + out[key] = ts.ScriptTarget.ES2018; + break; + case "es2019": + out[key] = ts.ScriptTarget.ES2019; + break; + case "es2020": + out[key] = ts.ScriptTarget.ES2020; + break; + case "esnext": + out[key] = ts.ScriptTarget.ESNext; + break; + default: + throw new TypeError("Unexpected emit target."); + } + default: + out[key] = options[key]; + } + } + return out as ts.CompilerOptions; +} + +/** An array of TypeScript diagnostic types we ignore. */ +export const ignoredDiagnostics = [ + // TS1103: 'for-await-of' statement is only allowed within an async function + // or async generator. + 1103, + // TS1308: 'await' expression is only allowed within an async function. + 1308, + // TS2691: An import path cannot end with a '.ts' extension. Consider + // importing 'bad-module' instead. + 2691, + // TS5009: Cannot find the common subdirectory path for the input files. + 5009, + // TS5055: Cannot write file + // 'http://localhost:4545/tests/subdir/mt_application_x_javascript.j4.js' + // because it would overwrite input file. + 5055, + // TypeScript is overly opinionated that only CommonJS modules kinds can + // support JSON imports. Allegedly this was fixed in + // Microsoft/TypeScript#26825 but that doesn't seem to be working here, + // so we will ignore complaints about this compiler setting. + 5070 +]; + +/** When doing a host configuration, processing the response and logging out + * and options which were ignored. */ +export function processConfigureResponse( + configResult: ConfigureResponse, + configPath: string +): ts.Diagnostic[] | undefined { + const { ignoredOptions, diagnostics } = configResult; + if (ignoredOptions) { + console.warn( + yellow(`Unsupported compiler options in "${configPath}"\n`) + + cyan(` The following options were ignored:\n`) + + ` ${ignoredOptions.map((value): string => bold(value)).join(", ")}` + ); + } + return diagnostics; +} diff --git a/cli/js/deno.ts b/cli/js/deno.ts index 971e99f4795248..9bc3bec9b866d0 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -97,6 +97,7 @@ export { ProcessStatus, Signal } from "./process.ts"; +export { transpileOnly, compile, bundle } from "./compiler_api.ts"; export { inspect, customInspect } from "./console.ts"; export { build, OperatingSystem, Arch } from "./build.ts"; export { version } from "./version.ts"; diff --git a/cli/js/diagnostics.ts b/cli/js/diagnostics.ts index df4a9a3d36709e..f0171bac61e7ec 100644 --- a/cli/js/diagnostics.ts +++ b/cli/js/diagnostics.ts @@ -64,152 +64,3 @@ export interface Diagnostic { /** An array of diagnostic items. */ items: DiagnosticItem[]; } - -interface SourceInformation { - sourceLine: string; - lineNumber: number; - scriptResourceName: string; - startColumn: number; - endColumn: number; -} - -function fromDiagnosticCategory( - category: ts.DiagnosticCategory -): DiagnosticCategory { - switch (category) { - case ts.DiagnosticCategory.Error: - return DiagnosticCategory.Error; - case ts.DiagnosticCategory.Message: - return DiagnosticCategory.Info; - case ts.DiagnosticCategory.Suggestion: - return DiagnosticCategory.Suggestion; - case ts.DiagnosticCategory.Warning: - return DiagnosticCategory.Warning; - default: - throw new Error( - `Unexpected DiagnosticCategory: "${category}"/"${ts.DiagnosticCategory[category]}"` - ); - } -} - -function getSourceInformation( - sourceFile: ts.SourceFile, - start: number, - length: number -): SourceInformation { - const scriptResourceName = sourceFile.fileName; - const { - line: lineNumber, - character: startColumn - } = sourceFile.getLineAndCharacterOfPosition(start); - const endPosition = sourceFile.getLineAndCharacterOfPosition(start + length); - const endColumn = - lineNumber === endPosition.line ? endPosition.character : startColumn; - const lastLineInFile = sourceFile.getLineAndCharacterOfPosition( - sourceFile.text.length - ).line; - const lineStart = sourceFile.getPositionOfLineAndCharacter(lineNumber, 0); - const lineEnd = - lineNumber < lastLineInFile - ? sourceFile.getPositionOfLineAndCharacter(lineNumber + 1, 0) - : sourceFile.text.length; - const sourceLine = sourceFile.text - .slice(lineStart, lineEnd) - .replace(/\s+$/g, "") - .replace("\t", " "); - return { - sourceLine, - lineNumber, - scriptResourceName, - startColumn, - endColumn - }; -} - -/** Converts a TypeScript diagnostic message chain to a Deno one. */ -function fromDiagnosticMessageChain( - messageChain: ts.DiagnosticMessageChain[] | undefined -): DiagnosticMessageChain[] | undefined { - if (!messageChain) { - return undefined; - } - - return messageChain.map(({ messageText: message, code, category, next }) => { - return { - message, - code, - category: fromDiagnosticCategory(category), - next: fromDiagnosticMessageChain(next) - }; - }); -} - -/** Parse out information from a TypeScript diagnostic structure. */ -function parseDiagnostic( - item: ts.Diagnostic | ts.DiagnosticRelatedInformation -): DiagnosticItem { - const { - messageText, - category: sourceCategory, - code, - file, - start: startPosition, - length - } = item; - const sourceInfo = - file && startPosition && length - ? getSourceInformation(file, startPosition, length) - : undefined; - const endPosition = - startPosition && length ? startPosition + length : undefined; - const category = fromDiagnosticCategory(sourceCategory); - - let message: string; - let messageChain: DiagnosticMessageChain | undefined; - if (typeof messageText === "string") { - message = messageText; - } else { - message = messageText.messageText; - messageChain = fromDiagnosticMessageChain([messageText])![0]; - } - - const base = { - message, - messageChain, - code, - category, - startPosition, - endPosition - }; - - return sourceInfo ? { ...base, ...sourceInfo } : base; -} - -/** Convert a diagnostic related information array into a Deno diagnostic - * array. */ -function parseRelatedInformation( - relatedInformation: readonly ts.DiagnosticRelatedInformation[] -): DiagnosticItem[] { - const result: DiagnosticItem[] = []; - for (const item of relatedInformation) { - result.push(parseDiagnostic(item)); - } - return result; -} - -/** Convert TypeScript diagnostics to Deno diagnostics. */ -export function fromTypeScriptDiagnostic( - diagnostics: readonly ts.Diagnostic[] -): Diagnostic { - const items: DiagnosticItem[] = []; - for (const sourceDiagnostic of diagnostics) { - const item: DiagnosticItem = parseDiagnostic(sourceDiagnostic); - if (sourceDiagnostic.relatedInformation) { - item.relatedInformation = parseRelatedInformation( - sourceDiagnostic.relatedInformation - ); - } - items.push(item); - } - return { items }; -} diff --git a/cli/js/diagnostics_util.ts b/cli/js/diagnostics_util.ts new file mode 100644 index 00000000000000..cc384ebb0f7f6b --- /dev/null +++ b/cli/js/diagnostics_util.ts @@ -0,0 +1,160 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +// These utilities are used by compiler.ts to format TypeScript diagnostics +// into Deno Diagnostics. + +import { + Diagnostic, + DiagnosticCategory, + DiagnosticMessageChain, + DiagnosticItem +} from "./diagnostics.ts"; + +interface SourceInformation { + sourceLine: string; + lineNumber: number; + scriptResourceName: string; + startColumn: number; + endColumn: number; +} + +function fromDiagnosticCategory( + category: ts.DiagnosticCategory +): DiagnosticCategory { + switch (category) { + case ts.DiagnosticCategory.Error: + return DiagnosticCategory.Error; + case ts.DiagnosticCategory.Message: + return DiagnosticCategory.Info; + case ts.DiagnosticCategory.Suggestion: + return DiagnosticCategory.Suggestion; + case ts.DiagnosticCategory.Warning: + return DiagnosticCategory.Warning; + default: + throw new Error( + `Unexpected DiagnosticCategory: "${category}"/"${ts.DiagnosticCategory[category]}"` + ); + } +} + +function getSourceInformation( + sourceFile: ts.SourceFile, + start: number, + length: number +): SourceInformation { + const scriptResourceName = sourceFile.fileName; + const { + line: lineNumber, + character: startColumn + } = sourceFile.getLineAndCharacterOfPosition(start); + const endPosition = sourceFile.getLineAndCharacterOfPosition(start + length); + const endColumn = + lineNumber === endPosition.line ? endPosition.character : startColumn; + const lastLineInFile = sourceFile.getLineAndCharacterOfPosition( + sourceFile.text.length + ).line; + const lineStart = sourceFile.getPositionOfLineAndCharacter(lineNumber, 0); + const lineEnd = + lineNumber < lastLineInFile + ? sourceFile.getPositionOfLineAndCharacter(lineNumber + 1, 0) + : sourceFile.text.length; + const sourceLine = sourceFile.text + .slice(lineStart, lineEnd) + .replace(/\s+$/g, "") + .replace("\t", " "); + return { + sourceLine, + lineNumber, + scriptResourceName, + startColumn, + endColumn + }; +} + +/** Converts a TypeScript diagnostic message chain to a Deno one. */ +function fromDiagnosticMessageChain( + messageChain: ts.DiagnosticMessageChain[] | undefined +): DiagnosticMessageChain[] | undefined { + if (!messageChain) { + return undefined; + } + + return messageChain.map(({ messageText: message, code, category, next }) => { + return { + message, + code, + category: fromDiagnosticCategory(category), + next: fromDiagnosticMessageChain(next) + }; + }); +} + +/** Parse out information from a TypeScript diagnostic structure. */ +function parseDiagnostic( + item: ts.Diagnostic | ts.DiagnosticRelatedInformation +): DiagnosticItem { + const { + messageText, + category: sourceCategory, + code, + file, + start: startPosition, + length + } = item; + const sourceInfo = + file && startPosition && length + ? getSourceInformation(file, startPosition, length) + : undefined; + const endPosition = + startPosition && length ? startPosition + length : undefined; + const category = fromDiagnosticCategory(sourceCategory); + + let message: string; + let messageChain: DiagnosticMessageChain | undefined; + if (typeof messageText === "string") { + message = messageText; + } else { + message = messageText.messageText; + messageChain = fromDiagnosticMessageChain([messageText])![0]; + } + + const base = { + message, + messageChain, + code, + category, + startPosition, + endPosition + }; + + return sourceInfo ? { ...base, ...sourceInfo } : base; +} + +/** Convert a diagnostic related information array into a Deno diagnostic + * array. */ +function parseRelatedInformation( + relatedInformation: readonly ts.DiagnosticRelatedInformation[] +): DiagnosticItem[] { + const result: DiagnosticItem[] = []; + for (const item of relatedInformation) { + result.push(parseDiagnostic(item)); + } + return result; +} + +/** Convert TypeScript diagnostics to Deno diagnostics. */ +export function fromTypeScriptDiagnostic( + diagnostics: readonly ts.Diagnostic[] +): Diagnostic { + const items: DiagnosticItem[] = []; + for (const sourceDiagnostic of diagnostics) { + const item: DiagnosticItem = parseDiagnostic(sourceDiagnostic); + if (sourceDiagnostic.relatedInformation) { + item.relatedInformation = parseRelatedInformation( + sourceDiagnostic.relatedInformation + ); + } + items.push(item); + } + return { items }; +} diff --git a/cli/js/dispatch.ts b/cli/js/dispatch.ts index b7fa94e4bde947..bb0861e9e86b6d 100644 --- a/cli/js/dispatch.ts +++ b/cli/js/dispatch.ts @@ -18,6 +18,7 @@ export let OP_START: number; export let OP_APPLY_SOURCE_MAP: number; export let OP_FORMAT_ERROR: number; export let OP_CACHE: number; +export let OP_RESOLVE_MODULES: number; export let OP_FETCH_SOURCE_FILES: number; export let OP_OPEN: number; export let OP_CLOSE: number; @@ -69,6 +70,8 @@ export let OP_FETCH_ASSET: number; export let OP_DIAL_TLS: number; export let OP_HOSTNAME: number; export let OP_OPEN_PLUGIN: number; +export let OP_COMPILE: number; +export let OP_TRANSPILE: number; const PLUGIN_ASYNC_HANDLER_MAP: Map = new Map(); @@ -120,6 +123,8 @@ export function asyncMsgFromRust(opId: number, ui8: Uint8Array): void { case OP_MAKE_TEMP_DIR: case OP_DIAL_TLS: case OP_FETCH_SOURCE_FILES: + case OP_COMPILE: + case OP_TRANSPILE: json.asyncMsgFromRust(opId, ui8); break; default: diff --git a/cli/js/globals.ts b/cli/js/globals.ts index eeded2f449f1ee..d06bc2f6fe19ae 100644 --- a/cli/js/globals.ts +++ b/cli/js/globals.ts @@ -62,6 +62,8 @@ declare global { interface Object { [consoleTypes.customInspect]?(): string; } + + const console: consoleTypes.Console; } // A self reference to the global object. diff --git a/cli/js/lib.deno_runtime.d.ts b/cli/js/lib.deno_runtime.d.ts index 85052dd16c9174..daeb0bf0bbe0b6 100644 --- a/cli/js/lib.deno_runtime.d.ts +++ b/cli/js/lib.deno_runtime.d.ts @@ -1493,6 +1493,410 @@ declare namespace Deno { export const version: Version; export {}; + // @url js/diagnostics.d.ts + + /** The log category for a diagnostic message */ + export enum DiagnosticCategory { + Log = 0, + Debug = 1, + Info = 2, + Error = 3, + Warning = 4, + Suggestion = 5 + } + + export interface DiagnosticMessageChain { + message: string; + category: DiagnosticCategory; + code: number; + next?: DiagnosticMessageChain[]; + } + + export interface DiagnosticItem { + /** A string message summarizing the diagnostic. */ + message: string; + + /** An ordered array of further diagnostics. */ + messageChain?: DiagnosticMessageChain; + + /** Information related to the diagnostic. This is present when there is a + * suggestion or other additional diagnostic information */ + relatedInformation?: DiagnosticItem[]; + + /** The text of the source line related to the diagnostic */ + sourceLine?: string; + + /** The line number that is related to the diagnostic */ + lineNumber?: number; + + /** The name of the script resource related to the diagnostic */ + scriptResourceName?: string; + + /** The start position related to the diagnostic */ + startPosition?: number; + + /** The end position related to the diagnostic */ + endPosition?: number; + + /** The category of the diagnostic */ + category: DiagnosticCategory; + + /** A number identifier */ + code: number; + + /** The the start column of the sourceLine related to the diagnostic */ + startColumn?: number; + + /** The end column of the sourceLine related to the diagnostic */ + endColumn?: number; + } + + export interface Diagnostic { + /** An array of diagnostic items. */ + items: DiagnosticItem[]; + } + + // @url js/compiler_api.ts + + /** A specific subset TypeScript compiler options that can be supported by + * the Deno TypeScript compiler. */ + export interface CompilerOptions { + /** Allow JavaScript files to be compiled. Defaults to `true`. */ + allowJs?: boolean; + + /** Allow default imports from modules with no default export. This does not + * affect code emit, just typechecking. Defaults to `false`. */ + allowSyntheticDefaultImports?: boolean; + + /** Allow accessing UMD globals from modules. Defaults to `false`. */ + allowUmdGlobalAccess?: boolean; + + /** Do not report errors on unreachable code. Defaults to `false`. */ + allowUnreachableCode?: boolean; + + /** Do not report errors on unused labels. Defaults to `false` */ + allowUnusedLabels?: boolean; + + /** Parse in strict mode and emit `"use strict"` for each source file. + * Defaults to `true`. */ + alwaysStrict?: boolean; + + /** Base directory to resolve non-relative module names. Defaults to + * `undefined`. */ + baseUrl?: string; + + /** Report errors in `.js` files. Use in conjunction with `allowJs`. Defaults + * to `false`. */ + checkJs?: boolean; + + /** Generates corresponding `.d.ts` file. Defaults to `false`. */ + declaration?: boolean; + + /** Output directory for generated declaration files. */ + declarationDir?: string; + + /** Generates a source map for each corresponding `.d.ts` file. Defaults to + * `false`. */ + declarationMap?: boolean; + + /** Provide full support for iterables in `for..of`, spread and + * destructuring when targeting ES5 or ES3. Defaults to `false`. */ + downlevelIteration?: boolean; + + /** Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. + * Defaults to `false`. */ + emitBOM?: boolean; + + /** Only emit `.d.ts` declaration files. Defaults to `false`. */ + emitDeclarationOnly?: boolean; + + /** Emit design-type metadata for decorated declarations in source. See issue + * [microsoft/TypeScript#2577](https://github.com/Microsoft/TypeScript/issues/2577) + * for details. Defaults to `false`. */ + emitDecoratorMetadata?: boolean; + + /** Emit `__importStar` and `__importDefault` helpers for runtime babel + * ecosystem compatibility and enable `allowSyntheticDefaultImports` for type + * system compatibility. Defaults to `true`. */ + esModuleInterop?: boolean; + + /** Enables experimental support for ES decorators. Defaults to `false`. */ + experimentalDecorators?: boolean; + + /** Emit a single file with source maps instead of having a separate file. + * Defaults to `false`. */ + inlineSourceMap?: boolean; + + /** 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 `false`. */ + isolatedModules?: boolean; + + /** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`. + * Defaults to `"react"`. */ + jsx?: "react" | "preserve" | "react-native"; + + /** Specify the JSX factory function to use when targeting react JSX emit, + * e.g. `React.createElement` or `h`. Defaults to `React.createElement`. */ + jsxFactory?: string; + + /** Resolve keyof to string valued property names only (no numbers or + * symbols). Defaults to `false`. */ + keyofStringsOnly?: string; + + /** Emit class fields with ECMAScript-standard semantics. Defaults to `false`. + * Does not apply to `"esnext"` target. */ + useDefineForClassFields?: boolean; + + /** The locale to use to show error messages. */ + locale?: string; + + /** Specifies the location where debugger should locate map files instead of + * generated locations. Use this flag if the `.map` files will be located at + * run-time in a different location than the `.js` files. The location + * specified will be embedded in the source map to direct the debugger where + * the map files will be located. Defaults to `undefined`. */ + mapRoot?: string; + + /** Specify the module format for the emitted code. Defaults to + * `"esnext"`. */ + module?: + | "none" + | "commonjs" + | "amd" + | "system" + | "umd" + | "es6" + | "es2015" + | "esnext"; + + /** Do not generate custom helper functions like `__extends` in compiled + * output. Defaults to `false`. */ + noEmitHelpers?: boolean; + + /** Report errors for fallthrough cases in switch statement. Defaults to + * `false`. */ + noFallthroughCasesInSwitch?: boolean; + + /** Raise error on expressions and declarations with an implied any type. + * Defaults to `true`. */ + noImplicitAny?: boolean; + + /** Report an error when not all code paths in function return a value. + * Defaults to `false`. */ + noImplicitReturns?: boolean; + + /** Raise error on `this` expressions with an implied `any` type. Defaults to + * `true`. */ + noImplicitThis?: boolean; + + /** Do not emit `"use strict"` directives in module output. Defaults to + * `false`. */ + noImplicitUseStrict?: 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; + + /** Report errors on unused locals. Defaults to `false`. */ + noUnusedLocals?: boolean; + + /** Report errors on unused parameters. Defaults to `false`. */ + noUnusedParameters?: boolean; + + /** Redirect output structure to the directory. This only impacts + * `Deno.compile` and only changes the emitted file names. Defaults to + * `undefined`. */ + outDir?: string; + + /** List of path mapping entries for module names to locations relative to the + * `baseUrl`. Defaults to `undefined`. */ + paths?: Record; + + /** Do not erase const enum declarations in generated code. Defaults to + * `false`. */ + preserveConstEnums?: boolean; + + /** Remove all comments except copy-right header comments beginning with + * `/*!`. Defaults to `true`. */ + removeComments?: boolean; + + /** Include modules imported with `.json` extension. Defaults to `true`. */ + resolveJsonModule?: boolean; + + /** Specifies the root directory of input files. Only use to control the + * output directory structure with `outDir`. Defaults to `undefined`. */ + rootDir?: string; + + /** List of _root_ folders whose combined content represent the structure of + * the project at runtime. Defaults to `undefined`. */ + rootDirs?: string[]; + + /** Generates corresponding `.map` file. Defaults to `false`. */ + sourceMap?: boolean; + + /** Specifies the location where debugger should locate TypeScript files + * instead of source locations. Use this flag if the sources will be located + * at run-time in a different location than that at design-time. The location + * specified will be embedded in the sourceMap to direct the debugger where + * the source files will be located. Defaults to `undefined`. */ + sourceRoot?: string; + + /** Enable all strict type checking options. Enabling `strict` enables + * `noImplicitAny`, `noImplicitThis`, `alwaysStrict`, `strictBindCallApply`, + * `strictNullChecks`, `strictFunctionTypes` and + * `strictPropertyInitialization`. Defaults to `true`. */ + strict?: boolean; + + /** Enable stricter checking of the `bind`, `call`, and `apply` methods on + * functions. Defaults to `true`. */ + strictBindCallApply?: boolean; + + /** Disable bivariant parameter checking for function types. Defaults to + * `true`. */ + strictFunctionTypes?: boolean; + + /** Ensure non-undefined class properties are initialized in the constructor. + * This option requires `strictNullChecks` be enabled in order to take effect. + * Defaults to `true`. */ + strictPropertyInitialization?: boolean; + + /** In strict null checking mode, the `null` and `undefined` values are not in + * the domain of every type and are only assignable to themselves and `any` + * (the one exception being that `undefined` is also assignable to `void`). */ + strictNullChecks?: boolean; + + /** Suppress excess property checks for object literals. Defaults to + * `false`. */ + suppressExcessPropertyErrors?: boolean; + + /** Suppress `noImplicitAny` errors for indexing objects lacking index + * signatures. */ + suppressImplicitAnyIndexErrors?: boolean; + + /** Specify ECMAScript target version. Defaults to `esnext`. */ + target?: + | "es3" + | "es5" + | "es6" + | "es2015" + | "es2016" + | "es2017" + | "es2018" + | "es2019" + | "es2020" + | "esnext"; + + /** List of names of type definitions to include. Defaults to `undefined`. */ + types?: string[]; + } + + /** 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; + } + + /** Takes a set of TypeScript sources and resolves with 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. + * + * 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. + * Many of the options related to type checking and emitting + * type declaration files will have no impact on the output. + */ + export function transpileOnly( + sources: Record, + options?: CompilerOptions + ): Promise>; + + /** Takes a root module name, any optionally a record set of sources. Resolves + * with a compiled set of modules. 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. + * + * 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]>; + + /** Takes a root module name, and optionally a record set of sources. Resolves + * with a single JavaScript string 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. + * + * 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. + */ + export function bundle( + rootName: string, + sources?: Record, + options?: CompilerOptions + ): Promise<[Diagnostic[] | undefined, string]>; + // @url js/deno.d.ts export const args: string[]; diff --git a/cli/js/unit_test_runner.ts b/cli/js/unit_test_runner.ts index c2c654696a6330..dd095058f179a7 100755 --- a/cli/js/unit_test_runner.ts +++ b/cli/js/unit_test_runner.ts @@ -9,14 +9,14 @@ import { interface TestResult { perms: string; - output: string; + output?: string; result: number; } function permsToCliFlags(perms: Permissions): string[] { return Object.keys(perms) - .map((key): string => { - if (!perms[key]) return ""; + .map(key => { + if (!perms[key as keyof Permissions]) return ""; const cliFlag = key.replace( /\.?([A-Z])/g, diff --git a/cli/js/unit_tests.ts b/cli/js/unit_tests.ts index 3b4e6de68e2d12..d886d6f5d6ea14 100644 --- a/cli/js/unit_tests.ts +++ b/cli/js/unit_tests.ts @@ -9,6 +9,7 @@ import "./buffer_test.ts"; import "./build_test.ts"; import "./chmod_test.ts"; import "./chown_test.ts"; +import "./compiler_api_test.ts"; import "./console_test.ts"; import "./copy_file_test.ts"; import "./custom_event_test.ts"; diff --git a/cli/js/util.ts b/cli/js/util.ts index 1a0d8df2e33ddf..928794beae3a46 100644 --- a/cli/js/util.ts +++ b/cli/js/util.ts @@ -126,6 +126,7 @@ export function isObject(o: unknown): o is object { } // Returns whether o is iterable. +// @internal export function isIterable( o: T ): o is T & Iterable<[P, K]> { @@ -224,6 +225,78 @@ export function splitNumberToParts(n: number): number[] { return [lower, higher]; } +// Constants used by `normalizeString` and `resolvePath` +export const CHAR_DOT = 46; /* . */ +export const CHAR_FORWARD_SLASH = 47; /* / */ + +/** Resolves `.` and `..` elements in a path with directory names */ +export function normalizeString( + path: string, + allowAboveRoot: boolean, + separator: string, + isPathSeparator: (code: number) => boolean +): string { + let res = ""; + let lastSegmentLength = 0; + let lastSlash = -1; + let dots = 0; + let code: number; + for (let i = 0, len = path.length; i <= len; ++i) { + if (i < len) code = path.charCodeAt(i); + else if (isPathSeparator(code!)) break; + else code = CHAR_FORWARD_SLASH; + + if (isPathSeparator(code)) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if ( + res.length < 2 || + lastSegmentLength !== 2 || + res.charCodeAt(res.length - 1) !== CHAR_DOT || + res.charCodeAt(res.length - 2) !== CHAR_DOT + ) { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf(separator); + if (lastSlashIndex === -1) { + res = ""; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); + } + lastSlash = i; + dots = 0; + continue; + } else if (res.length === 2 || res.length === 1) { + res = ""; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) res += `${separator}..`; + else res = ".."; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); + else res = path.slice(lastSlash + 1, i); + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === CHAR_DOT && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} + /** Return the common path shared by the `paths`. * * @param paths The set of paths to compare. @@ -269,3 +342,14 @@ export function humanFileSize(bytes: number): string { } while (Math.abs(bytes) >= thresh && u < units.length - 1); return `${bytes.toFixed(1)} ${units[u]}`; } + +// @internal +export function base64ToUint8Array(data: string): Uint8Array { + const binString = window.atob(data); + const size = binString.length; + const bytes = new Uint8Array(size); + for (let i = 0; i < size; i++) { + bytes[i] = binString.charCodeAt(i); + } + return bytes; +} diff --git a/cli/msg.rs b/cli/msg.rs index 90f95311815961..e1d0f07f26c2f2 100644 --- a/cli/msg.rs +++ b/cli/msg.rs @@ -97,5 +97,6 @@ pub fn enum_name_media_type(mt: MediaType) -> &'static str { #[derive(Clone, Copy, PartialEq, Debug)] pub enum CompilerRequestType { Compile = 0, - Bundle = 1, + RuntimeCompile = 1, + RuntimeTranspile = 2, } diff --git a/cli/ops/compiler.rs b/cli/ops/compiler.rs index bc85e5ca816d75..114a4e1f067bb5 100644 --- a/cli/ops/compiler.rs +++ b/cli/ops/compiler.rs @@ -1,14 +1,21 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use super::dispatch_json::{Deserialize, JsonOp, Value}; +use crate::compilers::runtime_compile_async; +use crate::compilers::runtime_transpile_async; use crate::futures::future::try_join_all; use crate::msg; use crate::ops::json_op; use crate::state::ThreadSafeState; use deno_core::Loader; use deno_core::*; +use std::collections::HashMap; pub fn init(i: &mut Isolate, s: &ThreadSafeState) { i.register_op("cache", s.core_op(json_op(s.stateful_op(op_cache)))); + i.register_op( + "resolve_modules", + s.core_op(json_op(s.stateful_op(op_resolve_modules))), + ); i.register_op( "fetch_source_files", s.core_op(json_op(s.stateful_op(op_fetch_source_files))), @@ -17,6 +24,8 @@ pub fn init(i: &mut Isolate, s: &ThreadSafeState) { "fetch_asset", s.core_op(json_op(s.stateful_op(op_fetch_asset))), ); + i.register_op("compile", s.core_op(json_op(s.stateful_op(op_compile)))); + i.register_op("transpile", s.core_op(json_op(s.stateful_op(op_transpile)))); } #[derive(Deserialize)] @@ -46,36 +55,62 @@ fn op_cache( Ok(JsonOp::Sync(json!({}))) } -#[derive(Deserialize)] -struct FetchSourceFilesArgs { +#[derive(Deserialize, Debug)] +struct SpecifiersReferrerArgs { specifiers: Vec, referrer: Option, } -fn op_fetch_source_files( +fn op_resolve_modules( state: &ThreadSafeState, args: Value, _data: Option, ) -> Result { - let args: FetchSourceFilesArgs = serde_json::from_value(args)?; + let args: SpecifiersReferrerArgs = serde_json::from_value(args)?; // TODO(ry) Maybe a security hole. Only the compiler worker should have access // to this. Need a test to demonstrate the hole. let is_dyn_import = false; - let (referrer, ref_specifier) = if let Some(referrer) = args.referrer { + let (referrer, is_main) = if let Some(referrer) = args.referrer { + (referrer, false) + } else { + ("".to_owned(), true) + }; + + let mut specifiers = vec![]; + + for specifier in &args.specifiers { + let resolved_specifier = + state.resolve(specifier, &referrer, is_main, is_dyn_import); + match resolved_specifier { + Ok(ms) => specifiers.push(ms.as_str().to_owned()), + Err(err) => return Err(err), + } + } + + Ok(JsonOp::Sync(json!(specifiers))) +} + +fn op_fetch_source_files( + state: &ThreadSafeState, + args: Value, + _data: Option, +) -> Result { + let args: SpecifiersReferrerArgs = serde_json::from_value(args)?; + + let ref_specifier = if let Some(referrer) = args.referrer { let specifier = ModuleSpecifier::resolve_url(&referrer) .expect("Referrer is not a valid specifier"); - (referrer, Some(specifier)) + Some(specifier) } else { - // main script import - (".".to_string(), None) + None }; let mut futures = vec![]; for specifier in &args.specifiers { let resolved_specifier = - state.resolve(specifier, &referrer, false, is_dyn_import)?; + ModuleSpecifier::resolve_url(&specifier).expect("Invalid specifier"); let fut = state .global_state .file_fetcher @@ -137,3 +172,46 @@ fn op_fetch_asset( panic!("op_fetch_asset bad asset {}", args.name) } } + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct CompileArgs { + root_name: String, + sources: Option>, + bundle: bool, + options: Option, +} + +fn op_compile( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: CompileArgs = serde_json::from_value(args)?; + Ok(JsonOp::Async(runtime_compile_async( + state.global_state.clone(), + &args.root_name, + &args.sources, + args.bundle, + &args.options, + ))) +} + +#[derive(Deserialize, Debug)] +struct TranspileArgs { + sources: HashMap, + options: Option, +} + +fn op_transpile( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: TranspileArgs = serde_json::from_value(args)?; + Ok(JsonOp::Async(runtime_transpile_async( + state.global_state.clone(), + &args.sources, + &args.options, + ))) +} diff --git a/cli/tests/error_011_bad_module_specifier.ts.out b/cli/tests/error_011_bad_module_specifier.ts.out index 276443e5bf64f3..7c100db13ad2cf 100644 --- a/cli/tests/error_011_bad_module_specifier.ts.out +++ b/cli/tests/error_011_bad_module_specifier.ts.out @@ -1,5 +1,8 @@ [WILDCARD]error: Uncaught ImportPrefixMissing: relative import path "bad-module.ts" not prefixed with / or ./ or ../ Imported from "[WILDCARD]/error_011_bad_module_specifier.ts" [WILDCARD]dispatch_json.ts:[WILDCARD] - at DenoError ([WILDCARD]errors.ts:[WILDCARD]) - at unwrapResponse ([WILDCARD]dispatch_json.ts:[WILDCARD]) - at sendAsync[WILDCARD] ([WILDCARD]dispatch_json.ts:[WILDCARD]) + at DenoError ($deno$/errors.ts:[WILDCARD]) + at unwrapResponse ($deno$/dispatch_json.ts:[WILDCARD]) + at sendSync ($deno$/dispatch_json.ts:[WILDCARD]) + at resolveModules ($deno$/compiler_imports.ts:[WILDCARD]) + at processImports ($deno$/compiler_imports.ts:[WILDCARD]) + at processImports ($deno$/compiler_imports.ts:[WILDCARD]) diff --git a/cli/tests/error_012_bad_dynamic_import_specifier.ts.out b/cli/tests/error_012_bad_dynamic_import_specifier.ts.out index 52e5b913e48b8c..095ca497bfc2c6 100644 --- a/cli/tests/error_012_bad_dynamic_import_specifier.ts.out +++ b/cli/tests/error_012_bad_dynamic_import_specifier.ts.out @@ -1,5 +1,8 @@ [WILDCARD]error: Uncaught ImportPrefixMissing: relative import path "bad-module.ts" not prefixed with / or ./ or ../ Imported from "[WILDCARD]/error_012_bad_dynamic_import_specifier.ts" [WILDCARD]dispatch_json.ts:[WILDCARD] - at DenoError ([WILDCARD]errors.ts:[WILDCARD]) - at unwrapResponse ([WILDCARD]dispatch_json.ts:[WILDCARD]) - at sendAsync[WILDCARD] ([WILDCARD]dispatch_json.ts:[WILDCARD]) + at DenoError ($deno$/errors.ts:[WILDCARD]) + at unwrapResponse ($deno$/dispatch_json.ts:[WILDCARD]) + at sendSync ($deno$/dispatch_json.ts:[WILDCARD]) + at resolveModules ($deno$/compiler_imports.ts:[WILDCARD]) + at processImports ($deno$/compiler_imports.ts:[WILDCARD]) + at processImports ($deno$/compiler_imports.ts:[WILDCARD]) diff --git a/cli/tests/error_type_definitions.ts.out b/cli/tests/error_type_definitions.ts.out index e21b6d7945273a..d2c6096aced33c 100644 --- a/cli/tests/error_type_definitions.ts.out +++ b/cli/tests/error_type_definitions.ts.out @@ -1,5 +1,8 @@ [WILDCARD]error: Uncaught ImportPrefixMissing: relative import path "baz" not prefixed with / or ./ or ../ Imported from "[WILDCARD]/type_definitions/bar.d.ts" [WILDCARD]dispatch_json.ts:[WILDCARD] - at DenoError ([WILDCARD]errors.ts:[WILDCARD]) - at unwrapResponse ([WILDCARD]dispatch_json.ts:[WILDCARD]) - at sendAsync[WILDCARD] ([WILDCARD]dispatch_json.ts:[WILDCARD]) + at DenoError ($deno$/errors.ts:[WILDCARD]) + at unwrapResponse ($deno$/dispatch_json.ts:[WILDCARD]) + at sendSync ($deno$/dispatch_json.ts:[WILDCARD]) + at resolveModules ($deno$/compiler_imports.ts:[WILDCARD]) + at processImports ($deno$/compiler_imports.ts:[WILDCARD]) + at processImports ($deno$/compiler_imports.ts:[WILDCARD]) diff --git a/std/manual.md b/std/manual.md index dabe3d73996df4..fe5cde9258b8b1 100644 --- a/std/manual.md +++ b/std/manual.md @@ -897,6 +897,132 @@ import { fib } from "./fib.wasm"; console.log(fib(20)); ``` +## Compiler API + +Deno supports runtime access to the built in TypeScript compiler. There are +three methods in the `Deno` namespace that provide this access. + +### `Deno.compile()` + +This works similar to `deno fetch` in that it can fetch code, compile it, but +not run it. It takes up to three arguments, the `rootName`, optionally +`sources`, and optionally `options`. The `rootName` is the root module which +will be used to generate the resulting program. This is like module name you +would pass on the command line in `deno --reload run example.ts`. The `sources` +is a hash where the key is the fully qualified module name, and the value is the +text source of the module. If `sources` is passed, Deno will resolve all the +modules from within that hash and not attempt to resolve them outside of Deno. +If `sources` are not provided, Deno will resolve modules as if the root module +had been passed on the command line. Deno will also cache any of these +resources. The `options` argument is a set of options of type +`Deno.CompilerOptions`, which is a subset of the TypeScript compiler options +which can be supported by Deno. + +The method resolves with a tuple where the first argument is any diagnostics +(syntax or type errors) related to the code, and a map of the code, where the +key would be the output filename and the value would be the content. + +An example of providing sources: + +```ts +const [diagnostics, emitMap] = await Deno.compile("/foo.ts", { + "/foo.ts": `import * as bar from "./bar.ts";\nconsole.log(bar);\n`, + "/bar.ts": `export const bar = "bar";\n` +}); + +assert(diagnostics == null); // ensuring no diagnostics are returned +console.log(emitMap); +``` + +We would expect map to contain 4 "files", named `/foo.js.map`, `/foo.js`, +`/bar.js.map`, and `/bar.js`. + +When not supplying resources, you can use local or remote modules, just like you +could do on the command line. So you could do something like this: + +```ts +const [diagnostics, emitMap] = await Deno.compile( + "https://deno.land/std/examples/welcome.ts" +); +``` + +We should get back in the `emitMap` a simple `console.log()` statement. + +### `Deno.bundle()` + +This works a lot like `deno bundle` does on the command line. It is also like +`Deno.compile()`, except instead of returning a map of files, it returns a +single string, which is a self-contained JavaScript ES module which will include +all of the code that was provided or resolved as well as exports of all the +exports of the root module that was provided. It takes up to three arguments, +the `rootName`, optionally `sources`, and optionally `options`. The `rootName` +is the root module which will be used to generate the resulting program. This is +like module name you would pass on the command line in `deno bundle example.ts`. +The `sources` is a hash where the key is the fully qualified module name, and +the value is the text source of the module. If `sources` is passed, Deno will +resolve all the modules from within that hash and not attempt to resolve them +outside of Deno. If `sources` are not provided, Deno will resolve modules as if +the root module had been passed on the command line. Deno will also cache any of +these resources. The `options` argument is a set of options of type +`Deno.CompilerOptions`, which is a subset of the TypeScript compiler options +which can be supported by Deno. + +An example of providing sources: + +```ts +const [diagnostics, emit] = await Deno.compile("/foo.ts", { + "/foo.ts": `import * as bar from "./bar.ts";\nconsole.log(bar);\n`, + "/bar.ts": `export const bar = "bar";\n` +}); + +assert(diagnostics == null); // ensuring no diagnostics are returned +console.log(emit); +``` + +We would expect `emit` to be the text for an ES module, which would contain the +output sources for both modules. + +When not supplying resources, you can use local or remote modules, just like you +could do on the command line. So you could do something like this: + +```ts +const [diagnostics, emit] = await Deno.compile( + "https://deno.land/std/http/server.ts" +); +``` + +We should get back in `emit` a self contained JavaScript ES module with all of +its dependencies resolved and exporting the same exports as the source module. + +### `Deno.transpileOnly()` + +This is based off of the TypeScript function `transpileModule()`. All this does +is "erase" any types from the modules and emit JavaScript. There is no type +checking and no resolution of dependencies. It accepts up to two arguments, the +first is a hash where the key is the module name and the value is the contents. +The only purpose of the module name is when putting information into a source +map, of what the source file name was. The second is optionally `options` which +is of type `Deno.CompilerOptions`. This is a subset of options which can be +supported by Deno. It resolves with a map where the key is the source module +name supplied, and the value is an object with a property of `source` which is +the output contents of the module, and optionally `map` which would be the +source map. By default, source maps are output, but can be turned off via the +`options` argument. + +An example: + +```ts +const result = await Deno.transpileOnly({ + "/foo.ts": `enum Foo { Foo, Bar, Baz };\n` +}); + +console.log(result["/foo.ts"].source); +console.log(result["/foo.ts"].map); +``` + +We would expect the `enum` would be rewritten to an IIFE which constructs the +enumerable, and the map to be defined. + ## Program lifecycle Deno supports browser compatible lifecycle events: `load` and `unload`. You can