// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // TODO(ry) Combine this implementation with //deno_typescript/compiler_main.js // This module is the entry point for "compiler" isolate, ie. the one // that is created when Deno needs to compile TS/WASM to JS. // // It provides a two functions that should be called by Rust: // - `bootstrapTsCompilerRuntime` // - `bootstrapWasmCompilerRuntime` // Either of these functions must be called when creating isolate // to properly setup runtime. // NOTE: this import has side effects! import "./ts_global.d.ts"; import { TranspileOnlyResult } from "./compiler_api.ts"; import { TS_SNAPSHOT_PROGRAM } from "./compiler_bootstrap.ts"; import { setRootExports } from "./compiler_bundler.ts"; import { CompilerHostTarget, defaultBundlerOptions, defaultRuntimeCompileOptions, defaultTranspileOptions, Host } from "./compiler_host.ts"; import { processImports, processLocalImports, resolveModules } from "./compiler_imports.ts"; import { createWriteFile, CompilerRequestType, convertCompilerOptions, ignoredDiagnostics, WriteFileState, processConfigureResponse, base64ToUint8Array } from "./compiler_util.ts"; import { Diagnostic, DiagnosticItem } from "./diagnostics.ts"; import { fromTypeScriptDiagnostic } from "./diagnostics_util.ts"; import { assert } from "./util.ts"; import * as util from "./util.ts"; import { bootstrapWorkerRuntime } from "./runtime_worker.ts"; interface CompilerRequestCompile { type: CompilerRequestType.Compile; target: CompilerHostTarget; rootNames: string[]; // TODO(ry) add compiler config to this interface. // options: ts.CompilerOptions; configPath?: string; config?: string; bundle?: boolean; outFile?: string; } interface CompilerRequestRuntimeCompile { type: CompilerRequestType.RuntimeCompile; target: CompilerHostTarget; rootName: string; sources?: Record; bundle?: boolean; options?: string; } interface CompilerRequestRuntimeTranspile { type: CompilerRequestType.RuntimeTranspile; sources: Record; options?: string; } /** 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; } type RuntimeCompileResult = [ undefined | DiagnosticItem[], Record ]; type RuntimeBundleResult = [undefined | DiagnosticItem[], string]; /** `Compile` are requests from the internals of Deno; eg. used when * the `run` or `bundle` subcommand is used. */ async function compile( request: CompilerRequestCompile ): Promise { const { bundle, config, configPath, outFile, rootNames, target } = request; util.log(">>> compile start", { rootNames, type: CompilerRequestType[request.type] }); // 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, target, 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); } // 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]), undefined, bundle || host.getCompilationSettings().checkJs ); 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, oldProgram: TS_SNAPSHOT_PROGRAM }); 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 }; util.log("<<< compile end", { rootNames, type: CompilerRequestType[request.type] }); return result; } /**`RuntimeCompile` are requests from a runtime user; it can be both * "compile" and "bundle". * * The process is similar to a request from the privileged * side, but unline `compile`, `runtimeCompile` allows to specify * additional file mappings which can be used instead of relying * on Deno defaults. */ async function runtimeCompile( request: CompilerRequestRuntimeCompile ): Promise { const { rootName, sources, options, bundle, target } = request; util.log(">>> runtime compile start", { rootName, bundle, sources: sources ? Object.keys(sources) : undefined }); // resolve the root name, if there are sources, the root name does not // get resolved const resolvedRootName = sources ? rootName : resolveModules([rootName])[0]; // if there are options, convert them into TypeScript compiler options, // and resolve any external file references let convertedOptions: ts.CompilerOptions | undefined; let additionalFiles: string[] | undefined; if (options) { const result = convertCompilerOptions(options); convertedOptions = result.options; additionalFiles = result.files; } const checkJsImports = bundle || (convertedOptions && convertedOptions.checkJs); // recursively process imports, loading each file into memory. If there // are sources, these files are pulled out of the there, otherwise the // files are retrieved from the privileged side const rootNames = sources ? processLocalImports( sources, [[resolvedRootName, resolvedRootName]], undefined, checkJsImports ) : await processImports( [[resolvedRootName, resolvedRootName]], undefined, checkJsImports ); if (additionalFiles) { // any files supplied in the configuration are resolved externally, // even if sources are provided const resolvedNames = resolveModules(additionalFiles); rootNames.push( ...(await processImports( resolvedNames.map(rn => [rn, rn]), undefined, checkJsImports )) ); } 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, target, writeFile })); const compilerOptions = [defaultRuntimeCompileOptions]; if (convertedOptions) { compilerOptions.push(convertedOptions); } if (bundle) { compilerOptions.push(defaultBundlerOptions); } host.mergeOptions(...compilerOptions); const program = ts.createProgram({ rootNames, options: host.getCompilationSettings(), host, oldProgram: TS_SNAPSHOT_PROGRAM }); if (bundle) { setRootExports(program, rootNames[0]); } const diagnostics = ts .getPreEmitDiagnostics(program) .filter(({ code }) => !ignoredDiagnostics.includes(code)); const emitResult = program.emit(); assert(emitResult.emitSkipped === false, "Unexpected skip of the emit."); assert(state.emitMap); util.log("<<< runtime compile finish", { rootName, sources: sources ? Object.keys(sources) : undefined, bundle, emitMap: Object.keys(state.emitMap) }); const maybeDiagnostics = diagnostics.length ? fromTypeScriptDiagnostic(diagnostics).items : undefined; if (bundle) { return [maybeDiagnostics, state.emitBundle] as RuntimeBundleResult; } else { return [maybeDiagnostics, state.emitMap] as RuntimeCompileResult; } } async function runtimeTranspile( request: CompilerRequestRuntimeTranspile ): Promise> { const result: Record = {}; const { sources, options } = request; const compilerOptions = options ? Object.assign( {}, defaultTranspileOptions, convertCompilerOptions(options).options ) : defaultTranspileOptions; for (const [fileName, inputText] of Object.entries(sources)) { const { outputText: source, sourceMapText: map } = ts.transpileModule( inputText, { fileName, compilerOptions } ); result[fileName] = { source, map }; } return result; } async function tsCompilerOnMessage({ data: request }: { data: CompilerRequest; }): Promise { switch (request.type) { case CompilerRequestType.Compile: { const result = await compile(request as CompilerRequestCompile); globalThis.postMessage(result); break; } case CompilerRequestType.RuntimeCompile: { const result = await runtimeCompile( request as CompilerRequestRuntimeCompile ); globalThis.postMessage(result); break; } case CompilerRequestType.RuntimeTranspile: { const result = await runtimeTranspile( request as CompilerRequestRuntimeTranspile ); globalThis.postMessage(result); break; } default: util.log( `!!! unhandled CompilerRequestType: ${ (request as CompilerRequest).type } (${CompilerRequestType[(request as CompilerRequest).type]})` ); } // Currently Rust shuts down worker after single request } async function wasmCompilerOnMessage({ data: binary }: { data: string; }): Promise { const buffer = base64ToUint8Array(binary); // @ts-ignore const compiled = await WebAssembly.compile(buffer); util.log(">>> WASM compile start"); const importList = Array.from( // @ts-ignore new Set(WebAssembly.Module.imports(compiled).map(({ module }) => module)) ); const exportList = Array.from( // @ts-ignore new Set(WebAssembly.Module.exports(compiled).map(({ name }) => name)) ); globalThis.postMessage({ importList, exportList }); util.log("<<< WASM compile end"); // Currently Rust shuts down worker after single request } function bootstrapTsCompilerRuntime(): void { bootstrapWorkerRuntime("TS"); globalThis.onmessage = tsCompilerOnMessage; } function bootstrapWasmCompilerRuntime(): void { bootstrapWorkerRuntime("WASM"); globalThis.onmessage = wasmCompilerOnMessage; } Object.defineProperties(globalThis, { bootstrapWasmCompilerRuntime: { value: bootstrapWasmCompilerRuntime, enumerable: false, writable: false, configurable: false }, bootstrapTsCompilerRuntime: { value: bootstrapTsCompilerRuntime, enumerable: false, writable: false, configurable: false } });