// 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 single functions that should be called by Rust: // - `bootstrapTsCompilerRuntime` // This functions must be called when creating isolate // to properly setup runtime. // Removes the `__proto__` for security reasons. This intentionally makes // Deno non compliant with ECMA-262 Annex B.2.2.1 // // eslint-disable-next-line @typescript-eslint/no-explicit-any delete Object.prototype.__proto__; ((window) => { const core = Deno.core; const { bold, cyan, yellow } = window.__bootstrap.colors; const { assert, log, notImplemented } = window.__bootstrap.util; const { DiagnosticCategory } = window.__bootstrap.diagnostics; const unstableDenoGlobalProperties = [ "umask", "linkSync", "link", "symlinkSync", "symlink", "loadavg", "osRelease", "openPlugin", "DiagnosticCategory", "DiagnosticMessageChain", "DiagnosticItem", "Diagnostic", "formatDiagnostics", "CompilerOptions", "TranspileOnlyResult", "transpileOnly", "compile", "bundle", "Location", "applySourceMap", "LinuxSignal", "MacOSSignal", "Signal", "SignalStream", "signal", "signals", "setRaw", "utimeSync", "utime", "ShutdownMode", "shutdown", "DatagramConn", "UnixListenOptions", "listen", "listenDatagram", "UnixConnectOptions", "connect", "StartTlsOptions", "startTls", "kill", "PermissionName", "PermissionState", "RunPermissionDescriptor", "ReadPermissionDescriptor", "WritePermissionDescriptor", "NetPermissionDescriptor", "EnvPermissionDescriptor", "PluginPermissionDescriptor", "HrtimePermissionDescriptor", "PermissionDescriptor", "Permissions", "PermissionStatus", "hostname", "ppid", ]; function transformMessageText(messageText, code) { switch (code) { case 2339: { const property = messageText .replace(/^Property '/, "") .replace(/' does not exist on type 'typeof Deno'\./, ""); if ( messageText.endsWith("on type 'typeof Deno'.") && unstableDenoGlobalProperties.includes(property) ) { return `${messageText} 'Deno.${property}' is an unstable API. Did you forget to run with the '--unstable' flag?`; } break; } case 2551: { const suggestionMessagePattern = / Did you mean '(.+)'\?$/; const property = messageText .replace(/^Property '/, "") .replace(/' does not exist on type 'typeof Deno'\./, "") .replace(suggestionMessagePattern, ""); const suggestion = messageText.match(suggestionMessagePattern); const replacedMessageText = messageText.replace( suggestionMessagePattern, "", ); if (suggestion && unstableDenoGlobalProperties.includes(property)) { const suggestedProperty = suggestion[1]; return `${replacedMessageText} 'Deno.${property}' is an unstable API. Did you forget to run with the '--unstable' flag, or did you mean '${suggestedProperty}'?`; } break; } } return messageText; } function fromDiagnosticCategory( category, ) { 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, start, length, ) { 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, }; } function fromDiagnosticMessageChain( messageChain, ) { if (!messageChain) { return undefined; } return messageChain.map(({ messageText, code, category, next }) => { const message = transformMessageText(messageText, code); return { message, code, category: fromDiagnosticCategory(category), next: fromDiagnosticMessageChain(next), }; }); } function parseDiagnostic( item, ) { 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; let messageChain; if (typeof messageText === "string") { message = transformMessageText(messageText, code); } else { message = transformMessageText(messageText.messageText, messageText.code); messageChain = fromDiagnosticMessageChain([messageText])[0]; } const base = { message, messageChain, code, category, startPosition, endPosition, }; return sourceInfo ? { ...base, ...sourceInfo } : base; } function parseRelatedInformation( relatedInformation, ) { const result = []; for (const item of relatedInformation) { result.push(parseDiagnostic(item)); } return result; } function fromTypeScriptDiagnostic( diagnostics, ) { const items = []; for (const sourceDiagnostic of diagnostics) { const item = parseDiagnostic(sourceDiagnostic); if (sourceDiagnostic.relatedInformation) { item.relatedInformation = parseRelatedInformation( sourceDiagnostic.relatedInformation, ); } items.push(item); } return { items }; } // We really don't want to depend on JSON dispatch during snapshotting, so // this op exchanges strings with Rust as raw byte arrays. function getAsset(name) { const opId = core.ops()["op_fetch_asset"]; const sourceCodeBytes = core.dispatch(opId, core.encode(name)); return core.decode(sourceCodeBytes); } // Constants used by `normalizeString` and `resolvePath` const CHAR_DOT = 46; /* . */ const CHAR_FORWARD_SLASH = 47; /* / */ // Using incremental compile APIs requires that all // paths must be either relative or absolute. Since // analysis in Rust operates on fully resolved URLs, // it makes sense to use the same scheme here. const ASSETS = "asset://"; const OUT_DIR = "deno://"; // This constant is passed to compiler settings when // doing incremental compiles. Contents of this // file are passed back to Rust and saved to $DENO_DIR. const TS_BUILD_INFO = "cache:///tsbuildinfo.json"; // TODO(Bartlomieju): this check should be done in Rust const IGNORED_COMPILER_OPTIONS = [ "allowSyntheticDefaultImports", "allowUmdGlobalAccess", "assumeChangesOnlyAffectDirectDependencies", "baseUrl", "build", "composite", "declaration", "declarationDir", "declarationMap", "diagnostics", "downlevelIteration", "emitBOM", "emitDeclarationOnly", "esModuleInterop", "extendedDiagnostics", "forceConsistentCasingInFileNames", "generateCpuProfile", "help", "importHelpers", "incremental", "inlineSourceMap", "inlineSources", "init", "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", ]; const DEFAULT_BUNDLER_OPTIONS = { allowJs: true, inlineSourceMap: false, module: ts.ModuleKind.System, outDir: undefined, outFile: `${OUT_DIR}/bundle.js`, // disabled until we have effective way to modify source maps sourceMap: false, }; const DEFAULT_INCREMENTAL_COMPILE_OPTIONS = { allowJs: false, allowNonTsExtensions: true, checkJs: false, esModuleInterop: true, incremental: true, inlineSourceMap: true, jsx: ts.JsxEmit.React, module: ts.ModuleKind.ESNext, outDir: OUT_DIR, resolveJsonModule: true, sourceMap: false, strict: true, stripComments: true, target: ts.ScriptTarget.ESNext, tsBuildInfoFile: TS_BUILD_INFO, }; const DEFAULT_COMPILE_OPTIONS = { allowJs: false, allowNonTsExtensions: true, checkJs: false, esModuleInterop: true, jsx: ts.JsxEmit.React, module: ts.ModuleKind.ESNext, outDir: OUT_DIR, sourceMap: true, strict: true, removeComments: true, target: ts.ScriptTarget.ESNext, }; const DEFAULT_TRANSPILE_OPTIONS = { esModuleInterop: true, inlineSourceMap: true, jsx: ts.JsxEmit.React, module: ts.ModuleKind.ESNext, removeComments: true, target: ts.ScriptTarget.ESNext, }; const DEFAULT_RUNTIME_COMPILE_OPTIONS = { outDir: undefined, }; const DEFAULT_RUNTIME_TRANSPILE_OPTIONS = { esModuleInterop: true, module: ts.ModuleKind.ESNext, sourceMap: true, scriptComments: true, target: ts.ScriptTarget.ESNext, }; const CompilerHostTarget = { Main: "main", Runtime: "runtime", Worker: "worker", }; // Warning! The values in this enum are duplicated in `cli/msg.rs` // Update carefully! const MediaType = { 0: "JavaScript", 1: "JSX", 2: "TypeScript", 3: "TSX", 4: "Json", 5: "Wasm", 6: "Unknown", JavaScript: 0, JSX: 1, TypeScript: 2, TSX: 3, Json: 4, Wasm: 5, Unknown: 6, }; function getExtension(fileName, mediaType) { 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.Wasm: // Custom marker for Wasm type. return ts.Extension.Js; case MediaType.Unknown: default: throw TypeError( `Cannot resolve extension for "${fileName}" with mediaType "${ MediaType[mediaType] }".`, ); } } /** A global cache of module source files that have been loaded. * This cache will be rewritten to be populated on compiler startup * with files provided from Rust in request message. */ const SOURCE_FILE_CACHE = new Map(); /** A map of maps which cache resolved specifier for each import in a file. * This cache is used so `resolveModuleNames` ops is called as few times * as possible. * * First map's key is "referrer" URL ("file://a/b/c/mod.ts") * Second map's key is "raw" import specifier ("./foo.ts") * Second map's value is resolved import URL ("file:///a/b/c/foo.ts") */ const RESOLVED_SPECIFIER_CACHE = new Map(); function configure( defaultOptions, source, path, cwd, ) { const { config, error } = ts.parseConfigFileTextToJson(path, source); if (error) { return { diagnostics: [error], options: defaultOptions }; } const { options, errors } = ts.convertCompilerOptionsFromJson( config.compilerOptions, cwd, ); const ignoredOptions = []; for (const key of Object.keys(options)) { if ( IGNORED_COMPILER_OPTIONS.includes(key) && (!(key in defaultOptions) || options[key] !== defaultOptions[key]) ) { ignoredOptions.push(key); delete options[key]; } } return { options: Object.assign({}, defaultOptions, options), ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined, diagnostics: errors.length ? errors : undefined, }; } class SourceFile { constructor(json) { this.processed = false; Object.assign(this, json); this.extension = getExtension(this.url, this.mediaType); } static addToCache(json) { if (SOURCE_FILE_CACHE.has(json.url)) { throw new TypeError("SourceFile already exists"); } const sf = new SourceFile(json); SOURCE_FILE_CACHE.set(sf.url, sf); return sf; } static getCached(url) { return SOURCE_FILE_CACHE.get(url); } static cacheResolvedUrl( resolvedUrl, rawModuleSpecifier, containingFile, ) { containingFile = containingFile || ""; let innerCache = RESOLVED_SPECIFIER_CACHE.get(containingFile); if (!innerCache) { innerCache = new Map(); RESOLVED_SPECIFIER_CACHE.set(containingFile, innerCache); } innerCache.set(rawModuleSpecifier, resolvedUrl); } static getResolvedUrl( moduleSpecifier, containingFile, ) { const containingCache = RESOLVED_SPECIFIER_CACHE.get(containingFile); if (containingCache) { return containingCache.get(moduleSpecifier); } return undefined; } } function getAssetInternal(filename) { const lastSegment = filename.split("/").pop(); const url = ts.libMap.has(lastSegment) ? ts.libMap.get(lastSegment) : lastSegment; const sourceFile = SourceFile.getCached(url); if (sourceFile) { return sourceFile; } const name = url.includes(".") ? url : `${url}.d.ts`; const sourceCode = getAsset(name); return SourceFile.addToCache({ url, filename: `${ASSETS}/${name}`, mediaType: MediaType.TypeScript, versionHash: "1", sourceCode, }); } class Host { #options = DEFAULT_COMPILE_OPTIONS; #target = ""; #writeFile = null; /* Deno specific APIs */ constructor({ bundle = false, incremental = false, target, unstable, writeFile, }) { this.#target = target; this.#writeFile = writeFile; if (bundle) { // options we need to change when we are generating a bundle Object.assign(this.#options, DEFAULT_BUNDLER_OPTIONS); } else if (incremental) { Object.assign(this.#options, DEFAULT_INCREMENTAL_COMPILE_OPTIONS); } if (unstable) { this.#options.lib = [ target === CompilerHostTarget.Worker ? "lib.deno.worker.d.ts" : "lib.deno.window.d.ts", "lib.deno.unstable.d.ts", ]; } } get options() { return this.#options; } configure( cwd, path, configurationText, ) { log("compiler::host.configure", path); const { options, ...result } = configure( this.#options, configurationText, path, cwd, ); this.#options = options; return result; } mergeOptions(...options) { Object.assign(this.#options, ...options); return Object.assign({}, this.#options); } /* TypeScript CompilerHost APIs */ fileExists(_fileName) { return notImplemented(); } getCanonicalFileName(fileName) { return fileName; } getCompilationSettings() { log("compiler::host.getCompilationSettings()"); return this.#options; } getCurrentDirectory() { return ""; } getDefaultLibFileName(_options) { log("compiler::host.getDefaultLibFileName()"); switch (this.#target) { case CompilerHostTarget.Main: case CompilerHostTarget.Runtime: return `${ASSETS}/lib.deno.window.d.ts`; case CompilerHostTarget.Worker: return `${ASSETS}/lib.deno.worker.d.ts`; } } getNewLine() { return "\n"; } getSourceFile( fileName, languageVersion, onError, shouldCreateNewSourceFile, ) { log("compiler::host.getSourceFile", fileName); try { assert(!shouldCreateNewSourceFile); const sourceFile = fileName.startsWith(ASSETS) ? getAssetInternal(fileName) : SourceFile.getCached(fileName); assert(sourceFile != null); if (!sourceFile.tsSourceFile) { assert(sourceFile.sourceCode != null); const tsSourceFileName = fileName.startsWith(ASSETS) ? sourceFile.filename : fileName; sourceFile.tsSourceFile = ts.createSourceFile( tsSourceFileName, sourceFile.sourceCode, languageVersion, ); sourceFile.tsSourceFile.version = sourceFile.versionHash; delete sourceFile.sourceCode; } return sourceFile.tsSourceFile; } catch (e) { if (onError) { onError(String(e)); } else { throw e; } return undefined; } } readFile(_fileName) { return notImplemented(); } resolveModuleNames( moduleNames, containingFile, ) { log("compiler::host.resolveModuleNames", { moduleNames, containingFile, }); const resolved = moduleNames.map((specifier) => { const maybeUrl = SourceFile.getResolvedUrl(specifier, containingFile); log("compiler::host.resolveModuleNames maybeUrl", { specifier, maybeUrl, }); let sourceFile = undefined; if (specifier.startsWith(ASSETS)) { sourceFile = getAssetInternal(specifier); } else if (typeof maybeUrl !== "undefined") { sourceFile = SourceFile.getCached(maybeUrl); } if (!sourceFile) { return undefined; } return { resolvedFileName: sourceFile.url, isExternalLibraryImport: specifier.startsWith(ASSETS), extension: sourceFile.extension, }; }); log(resolved); return resolved; } useCaseSensitiveFileNames() { return true; } writeFile( fileName, data, _writeByteOrderMark, _onError, sourceFiles, ) { log("compiler::host.writeFile", fileName); this.#writeFile(fileName, data, sourceFiles); } } class IncrementalCompileHost extends Host { #buildInfo = ""; constructor(options) { super({ ...options, incremental: true }); const { buildInfo } = options; if (buildInfo) { this.#buildInfo = buildInfo; } } readFile(fileName) { if (fileName == TS_BUILD_INFO) { return this.#buildInfo; } throw new Error("unreachable"); } } // NOTE: target doesn't really matter here, // this is in fact a mock host created just to // load all type definitions and snapshot them. let SNAPSHOT_HOST = new Host({ target: CompilerHostTarget.Main, writeFile() {}, }); const SNAPSHOT_COMPILER_OPTIONS = SNAPSHOT_HOST.getCompilationSettings(); // This is a hacky way of adding our libs to the libs available in TypeScript() // as these are internal APIs of TypeScript which maintain valid libs ts.libs.push("deno.ns", "deno.window", "deno.worker", "deno.shared_globals"); ts.libMap.set("deno.ns", "lib.deno.ns.d.ts"); ts.libMap.set("deno.window", "lib.deno.window.d.ts"); ts.libMap.set("deno.worker", "lib.deno.worker.d.ts"); ts.libMap.set("deno.shared_globals", "lib.deno.shared_globals.d.ts"); ts.libMap.set("deno.unstable", "lib.deno.unstable.d.ts"); // this pre-populates the cache at snapshot time of our library files, so they // are available in the future when needed. SNAPSHOT_HOST.getSourceFile( `${ASSETS}/lib.deno.ns.d.ts`, ts.ScriptTarget.ESNext, ); SNAPSHOT_HOST.getSourceFile( `${ASSETS}/lib.deno.window.d.ts`, ts.ScriptTarget.ESNext, ); SNAPSHOT_HOST.getSourceFile( `${ASSETS}/lib.deno.worker.d.ts`, ts.ScriptTarget.ESNext, ); SNAPSHOT_HOST.getSourceFile( `${ASSETS}/lib.deno.shared_globals.d.ts`, ts.ScriptTarget.ESNext, ); SNAPSHOT_HOST.getSourceFile( `${ASSETS}/lib.deno.unstable.d.ts`, ts.ScriptTarget.ESNext, ); // We never use this program; it's only created // during snapshotting to hydrate and populate // source file cache with lib declaration files. const _TS_SNAPSHOT_PROGRAM = ts.createProgram({ rootNames: [`${ASSETS}/bootstrap.ts`], options: SNAPSHOT_COMPILER_OPTIONS, host: SNAPSHOT_HOST, }); // Derference the snapshot host so it can be GCed SNAPSHOT_HOST = undefined; // This function is called only during snapshotting process const SYSTEM_LOADER = getAsset("system_loader.js"); const SYSTEM_LOADER_ES5 = getAsset("system_loader_es5.js"); function buildLocalSourceFileCache( sourceFileMap, ) { for (const entry of Object.values(sourceFileMap)) { assert(entry.sourceCode.length > 0); SourceFile.addToCache({ url: entry.url, filename: entry.url, mediaType: entry.mediaType, sourceCode: entry.sourceCode, versionHash: entry.versionHash, }); for (const importDesc of entry.imports) { let mappedUrl = importDesc.resolvedSpecifier; const importedFile = sourceFileMap[importDesc.resolvedSpecifier]; assert(importedFile); const isJsOrJsx = importedFile.mediaType === MediaType.JavaScript || importedFile.mediaType === MediaType.JSX; // If JS or JSX perform substitution for types if available if (isJsOrJsx) { if (importedFile.typeHeaders.length > 0) { const typeHeaders = importedFile.typeHeaders[0]; mappedUrl = typeHeaders.resolvedSpecifier; } else if (importDesc.resolvedTypeDirective) { mappedUrl = importDesc.resolvedTypeDirective; } else if (importedFile.typesDirectives.length > 0) { const typeDirective = importedFile.typesDirectives[0]; mappedUrl = typeDirective.resolvedSpecifier; } } mappedUrl = mappedUrl.replace("memory://", ""); SourceFile.cacheResolvedUrl(mappedUrl, importDesc.specifier, entry.url); } for (const fileRef of entry.referencedFiles) { SourceFile.cacheResolvedUrl( fileRef.resolvedSpecifier.replace("memory://", ""), fileRef.specifier, entry.url, ); } for (const fileRef of entry.libDirectives) { SourceFile.cacheResolvedUrl( fileRef.resolvedSpecifier.replace("memory://", ""), fileRef.specifier, entry.url, ); } } } function buildSourceFileCache( sourceFileMap, ) { for (const entry of Object.values(sourceFileMap)) { SourceFile.addToCache({ url: entry.url, filename: entry.url, mediaType: entry.mediaType, sourceCode: entry.sourceCode, versionHash: entry.versionHash, }); for (const importDesc of entry.imports) { let mappedUrl = importDesc.resolvedSpecifier; const importedFile = sourceFileMap[importDesc.resolvedSpecifier]; // IMPORTANT: due to HTTP redirects we might end up in situation // where URL points to a file with completely different URL. // In that case we take value of `redirect` field and cache // resolved specifier pointing to the value of the redirect. // It's not very elegant solution and should be rethinked. assert(importedFile); if (importedFile.redirect) { mappedUrl = importedFile.redirect; } const isJsOrJsx = importedFile.mediaType === MediaType.JavaScript || importedFile.mediaType === MediaType.JSX; // If JS or JSX perform substitution for types if available if (isJsOrJsx) { if (importedFile.typeHeaders.length > 0) { const typeHeaders = importedFile.typeHeaders[0]; mappedUrl = typeHeaders.resolvedSpecifier; } else if (importDesc.resolvedTypeDirective) { mappedUrl = importDesc.resolvedTypeDirective; } else if (importedFile.typesDirectives.length > 0) { const typeDirective = importedFile.typesDirectives[0]; mappedUrl = typeDirective.resolvedSpecifier; } } SourceFile.cacheResolvedUrl(mappedUrl, importDesc.specifier, entry.url); } for (const fileRef of entry.referencedFiles) { SourceFile.cacheResolvedUrl( fileRef.resolvedSpecifier, fileRef.specifier, entry.url, ); } for (const fileRef of entry.libDirectives) { SourceFile.cacheResolvedUrl( fileRef.resolvedSpecifier, fileRef.specifier, entry.url, ); } } } // Warning! The values in this enum are duplicated in `cli/msg.rs` // Update carefully! const CompilerRequestType = { Compile: 0, Transpile: 1, Bundle: 2, RuntimeCompile: 3, RuntimeBundle: 4, RuntimeTranspile: 5, }; function createBundleWriteFile(state) { return function writeFile( _fileName, data, sourceFiles, ) { assert(sourceFiles != null); assert(state.host); // we only support single root names for bundles assert(state.rootNames.length === 1); state.bundleOutput = buildBundle( state.rootNames[0], data, sourceFiles, state.host.options.target ?? ts.ScriptTarget.ESNext, ); }; } function createCompileWriteFile( state, ) { return function writeFile( fileName, data, sourceFiles, ) { const isBuildInfo = fileName === TS_BUILD_INFO; if (isBuildInfo) { assert(isBuildInfo); state.buildInfo = data; return; } assert(sourceFiles); assert(sourceFiles.length === 1); state.emitMap[fileName] = { filename: sourceFiles[0].fileName, contents: data, }; }; } function createRuntimeCompileWriteFile( state, ) { return function writeFile( fileName, data, sourceFiles, ) { assert(sourceFiles); assert(sourceFiles.length === 1); state.emitMap[fileName] = { filename: sourceFiles[0].fileName, contents: data, }; }; } function convertCompilerOptions(str) { const options = JSON.parse(str); const out = {}; const keys = Object.keys(options); const files = []; 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."); } break; case "types": const types = options[key]; assert(types); files.push(...types); break; default: out[key] = options[key]; } } return { options: out, files: files.length ? files : undefined, }; } const ignoredDiagnostics = [ // TS2306: File 'file:///Users/rld/src/deno/cli/tests/subdir/amd_like.js' is // not a module. 2306, // TS1375: 'await' expressions are only allowed at the top level of a file // when that file is a module, but this file has no imports or exports. // Consider adding an empty 'export {}' to make this file a module. 1375, // TS1103: 'for-await-of' statement is only allowed within an async function // or async generator. 1103, // 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/cli/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, // TS7016: Could not find a declaration file for module '...'. '...' // implicitly has an 'any' type. This is due to `allowJs` being off by // default but importing of a JavaScript module. 7016, ]; const stats = []; let statsStart = 0; function performanceStart() { stats.length = 0; // TODO(kitsonk) replace with performance.mark() when landed statsStart = performance.now(); ts.performance.enable(); } function performanceProgram({ program, fileCount, }) { if (program) { if ("getProgram" in program) { program = program.getProgram(); } stats.push({ key: "Files", value: program.getSourceFiles().length }); stats.push({ key: "Nodes", value: program.getNodeCount() }); stats.push({ key: "Identifiers", value: program.getIdentifierCount() }); stats.push({ key: "Symbols", value: program.getSymbolCount() }); stats.push({ key: "Types", value: program.getTypeCount() }); stats.push({ key: "Instantiations", value: program.getInstantiationCount(), }); } else if (fileCount != null) { stats.push({ key: "Files", value: fileCount }); } const programTime = ts.performance.getDuration("Program"); const bindTime = ts.performance.getDuration("Bind"); const checkTime = ts.performance.getDuration("Check"); const emitTime = ts.performance.getDuration("Emit"); stats.push({ key: "Parse time", value: programTime }); stats.push({ key: "Bind time", value: bindTime }); stats.push({ key: "Check time", value: checkTime }); stats.push({ key: "Emit time", value: emitTime }); stats.push({ key: "Total TS time", value: programTime + bindTime + checkTime + emitTime, }); } function performanceEnd() { // TODO(kitsonk) replace with performance.measure() when landed const duration = performance.now() - statsStart; stats.push({ key: "Compile time", value: duration }); return stats; } // TODO(Bartlomieju): this check should be done in Rust; there should be no function processConfigureResponse( configResult, configPath, ) { 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) => bold(value)).join(", ")}`, ); } return diagnostics; } function normalizeString(path) { let res = ""; let lastSegmentLength = 0; let lastSlash = -1; let dots = 0; let code; for (let i = 0, len = path.length; i <= len; ++i) { if (i < len) code = path.charCodeAt(i); else if (code === CHAR_FORWARD_SLASH) break; else code = CHAR_FORWARD_SLASH; if (code === CHAR_FORWARD_SLASH) { 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("/"); if (lastSlashIndex === -1) { res = ""; lastSegmentLength = 0; } else { res = res.slice(0, lastSlashIndex); lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); } lastSlash = i; dots = 0; continue; } else if (res.length === 2 || res.length === 1) { res = ""; lastSegmentLength = 0; lastSlash = i; dots = 0; continue; } } } else { if (res.length > 0) res += "/" + 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; } function commonPath(paths, sep = "/") { const [first = "", ...remaining] = paths; if (first === "" || remaining.length === 0) { return first.substring(0, first.lastIndexOf(sep) + 1); } const parts = first.split(sep); let endOfPrefix = parts.length; for (const path of remaining) { const compare = path.split(sep); for (let i = 0; i < endOfPrefix; i++) { if (compare[i] !== parts[i]) { endOfPrefix = i; } } if (endOfPrefix === 0) { return ""; } } const prefix = parts.slice(0, endOfPrefix).join(sep); return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`; } let rootExports; function normalizeUrl(rootName) { const match = /^(\S+:\/{2,3})(.+)$/.exec(rootName); if (match) { const [, protocol, path] = match; return `${protocol}${normalizeString(path)}`; } else { return rootName; } } function buildBundle( rootName, data, sourceFiles, target, ) { // when outputting to AMD and a single outfile, TypeScript makes up the module // specifiers which are used to define the modules, and doesn't expose them // publicly, so we have to try to replicate const sources = sourceFiles.map((sf) => sf.fileName); const sharedPath = commonPath(sources); rootName = normalizeUrl(rootName) .replace(sharedPath, "") .replace(/\.\w+$/i, ""); // If one of the modules requires support for top-level-await, TypeScript will // emit the execute function as an async function. When this is the case we // need to bubble up the TLA to the instantiation, otherwise we instantiate // synchronously. const hasTla = data.match(/execute:\sasync\sfunction\s/); let instantiate; if (rootExports && rootExports.length) { instantiate = hasTla ? `const __exp = await __instantiate("${rootName}", true);\n` : `const __exp = __instantiate("${rootName}", false);\n`; for (const rootExport of rootExports) { if (rootExport === "default") { instantiate += `export default __exp["${rootExport}"];\n`; } else { instantiate += `export const ${rootExport} = __exp["${rootExport}"];\n`; } } } else { instantiate = hasTla ? `await __instantiate("${rootName}", true);\n` : `__instantiate("${rootName}", false);\n`; } const es5Bundle = target === ts.ScriptTarget.ES3 || target === ts.ScriptTarget.ES5 || target === ts.ScriptTarget.ES2015 || target === ts.ScriptTarget.ES2016; return `${ es5Bundle ? SYSTEM_LOADER_ES5 : SYSTEM_LOADER }\n${data}\n${instantiate}`; } function setRootExports(program, rootModule) { // get a reference to the type checker, this will let us find symbols from // the AST. const checker = program.getTypeChecker(); // get a reference to the main source file for the bundle const mainSourceFile = program.getSourceFile(rootModule); assert(mainSourceFile); // retrieve the internal TypeScript symbol for this AST node const mainSymbol = checker.getSymbolAtLocation(mainSourceFile); if (!mainSymbol) { return; } rootExports = checker .getExportsOfModule(mainSymbol) // .getExportsOfModule includes type only symbols which are exported from // the module, so we need to try to filter those out. While not critical // someone looking at the bundle would think there is runtime code behind // that when there isn't. There appears to be no clean way of figuring that // out, so inspecting SymbolFlags that might be present that are type only .filter( (sym) => sym.flags & ts.SymbolFlags.Class || !( sym.flags & ts.SymbolFlags.Interface || sym.flags & ts.SymbolFlags.TypeLiteral || sym.flags & ts.SymbolFlags.Signature || sym.flags & ts.SymbolFlags.TypeParameter || sym.flags & ts.SymbolFlags.TypeAlias || sym.flags & ts.SymbolFlags.Type || sym.flags & ts.SymbolFlags.Namespace || sym.flags & ts.SymbolFlags.InterfaceExcludes || sym.flags & ts.SymbolFlags.TypeParameterExcludes || sym.flags & ts.SymbolFlags.TypeAliasExcludes ), ) .map((sym) => sym.getName()); } function compile({ allowJs, buildInfo, config, configPath, rootNames, target, unstable, cwd, sourceFileMap, type, performance, }) { if (performance) { performanceStart(); } log(">>> compile start", { rootNames, type: CompilerRequestType[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. const state = { rootNames, emitMap: {}, }; const host = new IncrementalCompileHost({ bundle: false, target, unstable, writeFile: createCompileWriteFile(state), rootNames, buildInfo, }); let diagnostics = []; host.mergeOptions({ allowJs }); // if there is a configuration supplied, we need to parse that if (config && config.length && configPath) { const configResult = host.configure(cwd, configPath, config); diagnostics = processConfigureResponse(configResult, configPath) || []; } buildSourceFileCache(sourceFileMap); // if there was a configuration and no diagnostics with it, we will continue // to generate the program and possibly emit it. if (diagnostics.length === 0) { const options = host.getCompilationSettings(); const program = ts.createIncrementalProgram({ rootNames, options, host, }); // TODO(bartlomieju): check if this is ok diagnostics = [ ...program.getConfigFileParsingDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getOptionsDiagnostics(), ...program.getGlobalDiagnostics(), ...program.getSemanticDiagnostics(), ]; diagnostics = diagnostics.filter( ({ code }) => !ignoredDiagnostics.includes(code), ); // We will only proceed with the emit if there are no diagnostics. if (diagnostics.length === 0) { const emitResult = program.emit(); // If `checkJs` is off we still might be compiling entry point JavaScript file // (if it has `.ts` imports), but it won't be emitted. In that case we skip // assertion. if (options.checkJs) { assert( emitResult.emitSkipped === false, "Unexpected skip of the emit.", ); } // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned // without casting. diagnostics = emitResult.diagnostics; } performanceProgram({ program }); } log("<<< compile end", { rootNames, type: CompilerRequestType[type] }); const stats = performance ? performanceEnd() : undefined; return { emitMap: state.emitMap, buildInfo: state.buildInfo, diagnostics: fromTypeScriptDiagnostic(diagnostics), stats, }; } function transpile({ config: configText, configPath, cwd, performance, sourceFiles, }) { if (performance) { performanceStart(); } log(">>> transpile start"); let compilerOptions; if (configText && configPath && cwd) { const { options, ...response } = configure( DEFAULT_TRANSPILE_OPTIONS, configText, configPath, cwd, ); const diagnostics = processConfigureResponse(response, configPath); if (diagnostics && diagnostics.length) { return { diagnostics: fromTypeScriptDiagnostic(diagnostics), emitMap: {}, }; } compilerOptions = options; } else { compilerOptions = Object.assign({}, DEFAULT_TRANSPILE_OPTIONS); } const emitMap = {}; let diagnostics = []; for (const { sourceCode, fileName } of sourceFiles) { const { outputText, sourceMapText, diagnostics: diags, } = ts.transpileModule(sourceCode, { fileName, compilerOptions, reportDiagnostics: true, }); if (diags) { diagnostics = diagnostics.concat(...diags); } emitMap[`${fileName}.js`] = { filename: fileName, contents: outputText }; // currently we inline source maps, but this is good logic to have if this // ever changes if (sourceMapText) { emitMap[`${fileName}.map`] = { filename: fileName, contents: sourceMapText, }; } } performanceProgram({ fileCount: sourceFiles.length }); const stats = performance ? performanceEnd() : undefined; log("<<< transpile end"); return { diagnostics: fromTypeScriptDiagnostic(diagnostics), emitMap, stats, }; } function bundle({ config, configPath, rootNames, target, unstable, cwd, sourceFileMap, type, }) { if (performance) { performanceStart(); } log(">>> bundle start", { rootNames, type: CompilerRequestType[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. const state = { rootNames, bundleOutput: undefined, }; const host = new Host({ bundle: true, target, unstable, writeFile: createBundleWriteFile(state), }); state.host = host; let diagnostics = []; // if there is a configuration supplied, we need to parse that if (config && config.length && configPath) { const configResult = host.configure(cwd, configPath, config); diagnostics = processConfigureResponse(configResult, configPath) || []; } buildSourceFileCache(sourceFileMap); // if there was a configuration and no diagnostics with it, we will continue // to generate the program and possibly emit it. if (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.length === 0) { // we only support a single root module when bundling assert(rootNames.length === 1); setRootExports(program, rootNames[0]); const emitResult = program.emit(); assert( emitResult.emitSkipped === false, "Unexpected skip of the emit.", ); // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned // without casting. diagnostics = emitResult.diagnostics; } if (performance) { performanceProgram({ program }); } } let bundleOutput; if (diagnostics.length === 0) { assert(state.bundleOutput); bundleOutput = state.bundleOutput; } const stats = performance ? performanceEnd() : undefined; const result = { bundleOutput, diagnostics: fromTypeScriptDiagnostic(diagnostics), stats, }; log("<<< bundle end", { rootNames, type: CompilerRequestType[type], }); return result; } function runtimeCompile( request, ) { const { options, rootNames, target, unstable, sourceFileMap } = request; log(">>> runtime compile start", { rootNames, }); // if there are options, convert them into TypeScript compiler options, // and resolve any external file references let convertedOptions; if (options) { const result = convertCompilerOptions(options); convertedOptions = result.options; } buildLocalSourceFileCache(sourceFileMap); const state = { rootNames, emitMap: {}, }; const host = new Host({ bundle: false, target, writeFile: createRuntimeCompileWriteFile(state), }); const compilerOptions = [DEFAULT_RUNTIME_COMPILE_OPTIONS]; if (convertedOptions) { compilerOptions.push(convertedOptions); } if (unstable) { compilerOptions.push({ lib: [ "deno.unstable", ...((convertedOptions && convertedOptions.lib) || ["deno.window"]), ], }); } host.mergeOptions(...compilerOptions); const program = ts.createProgram({ rootNames, options: host.getCompilationSettings(), host, }); const diagnostics = ts .getPreEmitDiagnostics(program) .filter(({ code }) => !ignoredDiagnostics.includes(code)); const emitResult = program.emit(); assert(emitResult.emitSkipped === false, "Unexpected skip of the emit."); log("<<< runtime compile finish", { rootNames, emitMap: Object.keys(state.emitMap), }); const maybeDiagnostics = diagnostics.length ? fromTypeScriptDiagnostic(diagnostics).items : []; return { diagnostics: maybeDiagnostics, emitMap: state.emitMap, }; } function runtimeBundle(request) { const { options, rootNames, target, unstable, sourceFileMap } = request; log(">>> runtime bundle start", { rootNames, }); // if there are options, convert them into TypeScript compiler options, // and resolve any external file references let convertedOptions; if (options) { const result = convertCompilerOptions(options); convertedOptions = result.options; } buildLocalSourceFileCache(sourceFileMap); const state = { rootNames, bundleOutput: undefined, }; const host = new Host({ bundle: true, target, writeFile: createBundleWriteFile(state), }); state.host = host; const compilerOptions = [DEFAULT_RUNTIME_COMPILE_OPTIONS]; if (convertedOptions) { compilerOptions.push(convertedOptions); } if (unstable) { compilerOptions.push({ lib: [ "deno.unstable", ...((convertedOptions && convertedOptions.lib) || ["deno.window"]), ], }); } compilerOptions.push(DEFAULT_BUNDLER_OPTIONS); host.mergeOptions(...compilerOptions); const program = ts.createProgram({ rootNames, options: host.getCompilationSettings(), host, }); 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."); log("<<< runtime bundle finish", { rootNames, }); const maybeDiagnostics = diagnostics.length ? fromTypeScriptDiagnostic(diagnostics).items : []; return { diagnostics: maybeDiagnostics, output: state.bundleOutput, }; } function runtimeTranspile( request, ) { const result = {}; const { sources, options } = request; const compilerOptions = options ? Object.assign( {}, DEFAULT_RUNTIME_TRANSPILE_OPTIONS, convertCompilerOptions(options).options, ) : DEFAULT_RUNTIME_TRANSPILE_OPTIONS; for (const [fileName, inputText] of Object.entries(sources)) { const { outputText: source, sourceMapText: map } = ts.transpileModule( inputText, { fileName, compilerOptions, }, ); result[fileName] = { source, map }; } return Promise.resolve(result); } async function tsCompilerOnMessage({ data: request, }) { switch (request.type) { case CompilerRequestType.Compile: { const result = compile(request); globalThis.postMessage(result); break; } case CompilerRequestType.Transpile: { const result = transpile(request); globalThis.postMessage(result); break; } case CompilerRequestType.Bundle: { const result = bundle(request); globalThis.postMessage(result); break; } case CompilerRequestType.RuntimeCompile: { const result = runtimeCompile(request); globalThis.postMessage(result); break; } case CompilerRequestType.RuntimeBundle: { const result = runtimeBundle(request); globalThis.postMessage(result); break; } case CompilerRequestType.RuntimeTranspile: { const result = await runtimeTranspile(request); globalThis.postMessage(result); break; } default: log( `!!! unhandled CompilerRequestType: ${request.type} (${ CompilerRequestType[request.type] })`, ); } // Shutdown after single request globalThis.close(); } function bootstrapTsCompilerRuntime() { globalThis.bootstrap.workerRuntime("TS", false); globalThis.onmessage = tsCompilerOnMessage; } Object.defineProperties(globalThis, { bootstrap: { value: { ...globalThis.bootstrap, tsCompilerRuntime: bootstrapTsCompilerRuntime, }, configurable: true, writable: true, }, }); })(this);