mirror of
https://github.com/denoland/deno.git
synced 2024-11-01 09:24:20 -04:00
52b96fc22a
- Cleanup "tsCompilerOnMessage" by factoring out separate methods for each request type: * "compile" * "runtimeCompile" * "runtimeTranspile" - Simplify control flow of compiler workers by a) no longer calling "close()" in worker runtime after a single message; b) explicitly shutting down worker from host after a single message Co-authored-by: Ryan Dahl <ry@tinyclouds.org>
427 lines
12 KiB
TypeScript
427 lines
12 KiB
TypeScript
// 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
|
|
} 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<string, string>;
|
|
bundle?: boolean;
|
|
options?: string;
|
|
}
|
|
|
|
interface CompilerRequestRuntimeTranspile {
|
|
type: CompilerRequestType.RuntimeTranspile;
|
|
sources: Record<string, string>;
|
|
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<string, string>
|
|
];
|
|
|
|
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<CompileResult> {
|
|
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<RuntimeCompileResult | RuntimeBundleResult> {
|
|
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<Record<string, TranspileOnlyResult>> {
|
|
const result: Record<string, TranspileOnlyResult> = {};
|
|
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<void> {
|
|
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<void> {
|
|
const buffer = util.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
|
|
}
|
|
});
|