// Copyright 2018-2020 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 { atob, TextDecoder, TextEncoder } from "./web/text_encoding.ts"; import { core } from "./core.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("op_cache", { extension: ".map", moduleId, contents }); } else if ( emittedFileName.endsWith(".js") || emittedFileName.endsWith(".json") ) { // Compiled JavaScript sendSync("op_cache", { extension: ".js", moduleId, contents }); } else { assert(false, `Trying to cache unhandled file type "${emittedFileName}"`); } } const encoder = new TextEncoder(); const decoder = new TextDecoder(); /** Retrieve an asset from Rust. */ export function getAsset(name: string): string { const opId = core.ops()["op_fetch_asset"]; // We really don't want to depend on JSON dispatch during snapshotting, so // this op exchanges strings with Rust as raw byte arrays. const sourceCodeBytes = core.dispatch(opId, encoder.encode(name)); return decoder.decode(sourceCodeBytes!); } /** Generates a `writeFile` function which can be passed to the compiler `Host` * to use when emitting files. */ export function createWriteFile(state: WriteFileState): WriteFileCallback { const encoder = new TextEncoder(); 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(`${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); } }; } export interface ConvertCompilerOptionsResult { files?: string[]; options: ts.CompilerOptions; } /** 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 ): ConvertCompilerOptionsResult { const options: CompilerOptions = JSON.parse(str); const out: Record = {}; const keys = Object.keys(options) as Array; const files: string[] = []; 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 as ts.CompilerOptions, files: files.length ? files : undefined }; } /** An array of TypeScript diagnostic types we ignore. */ export 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 ]; /** 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; } // 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. * @param sep An optional separator to use. Defaults to `/`. * @internal */ export function commonPath(paths: string[], sep = "/"): string { 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}`; } /** Utility function to turn the number of bytes into a human readable * unit */ function humanFileSize(bytes: number): string { const thresh = 1000; if (Math.abs(bytes) < thresh) { return bytes + " B"; } const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; let u = -1; do { bytes /= thresh; ++u; } while (Math.abs(bytes) >= thresh && u < units.length - 1); return `${bytes.toFixed(1)} ${units[u]}`; } // @internal export function base64ToUint8Array(data: string): Uint8Array { const binString = 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; }