diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2020-03-11 10:53:06 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-11 10:53:06 +0100 |
commit | 99a0c6df79b903e4fe72ce066787039bdede3868 (patch) | |
tree | 1c0abdb964a2e052b593dc8fa3e515f76dbc0642 /cli/js/compiler | |
parent | 94f4c6807a34a564f567b984e71e36e9cb9c5005 (diff) |
reorg: cli/js/compiler/, move more API to cli/js/web/ (#4310)
- moves compiler implementation to "cli/js/compiler/" directory
- moves more APIs to "cli/js/web":
* "console.ts"
* "console_table.ts"
* "performance.ts"
* "timers.ts"
* "workers.ts"
- removes some dead code from "cli/js/"
Diffstat (limited to 'cli/js/compiler')
-rw-r--r-- | cli/js/compiler/api.ts | 409 | ||||
-rw-r--r-- | cli/js/compiler/bootstrap.ts | 52 | ||||
-rw-r--r-- | cli/js/compiler/bundler.ts | 103 | ||||
-rw-r--r-- | cli/js/compiler/host.ts | 329 | ||||
-rw-r--r-- | cli/js/compiler/imports.ts | 183 | ||||
-rw-r--r-- | cli/js/compiler/sourcefile.ts | 189 | ||||
-rw-r--r-- | cli/js/compiler/ts_global.d.ts | 26 | ||||
-rw-r--r-- | cli/js/compiler/type_directives.ts | 91 | ||||
-rw-r--r-- | cli/js/compiler/util.ts | 448 |
9 files changed, 1830 insertions, 0 deletions
diff --git a/cli/js/compiler/api.ts b/cli/js/compiler/api.ts new file mode 100644 index 000000000..e6e1d5eee --- /dev/null +++ b/cli/js/compiler/api.ts @@ -0,0 +1,409 @@ +// Copyright 2018-2020 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 { DiagnosticItem } from "../diagnostics.ts"; +import * as util from "../util.ts"; +import * as runtimeCompilerOps from "../ops/runtime_compiler.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; + + /** List of library files to be included in the compilation. If omitted, + * then the Deno main runtime libs are used. */ + lib?: string[]; + + /** 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<string, string[]>; + + /** 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`. + * + * The type definitions are resolved according to the normal Deno resolution + * irrespective of if sources are provided on the call. Like other Deno + * modules, there is no "magical" resolution. For example: + * + * Deno.compile( + * "./foo.js", + * undefined, + * { + * types: [ "./foo.d.ts", "https://deno.land/x/example/types.d.ts" ] + * } + * ); + * + */ + 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 async function transpileOnly( + sources: Record<string, string>, + options: CompilerOptions = {} +): Promise<Record<string, TranspileOnlyResult>> { + util.log("Deno.transpileOnly", { sources: Object.keys(sources), options }); + const payload = { + sources, + options: JSON.stringify(options) + }; + const result = await runtimeCompilerOps.transpile(payload); + return 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 async function compile( + rootName: string, + sources?: Record<string, string>, + options: CompilerOptions = {} +): Promise<[DiagnosticItem[] | undefined, Record<string, string>]> { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: JSON.stringify(options), + bundle: false + }; + util.log("Deno.compile", { + rootName: payload.rootName, + sources: !!sources, + options + }); + const result = await runtimeCompilerOps.compile(payload); + return 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 async function bundle( + rootName: string, + sources?: Record<string, string>, + options: CompilerOptions = {} +): Promise<[DiagnosticItem[] | undefined, string]> { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: JSON.stringify(options), + bundle: true + }; + util.log("Deno.bundle", { + rootName: payload.rootName, + sources: !!sources, + options + }); + const result = await runtimeCompilerOps.compile(payload); + return JSON.parse(result); +} diff --git a/cli/js/compiler/bootstrap.ts b/cli/js/compiler/bootstrap.ts new file mode 100644 index 000000000..d4642d041 --- /dev/null +++ b/cli/js/compiler/bootstrap.ts @@ -0,0 +1,52 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { CompilerHostTarget, Host } from "./host.ts"; +import { ASSETS } from "./sourcefile.ts"; +import { getAsset } from "./util.ts"; + +// NOTE: target doesn't really matter here, +// this is in fact a mock host created just to +// load all type definitions and snapshot them. +const host = new Host({ + target: CompilerHostTarget.Main, + writeFile(): void {} +}); +const options = 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"); + +// this pre-populates the cache at snapshot time of our library files, so they +// are available in the future when needed. +host.getSourceFile(`${ASSETS}/lib.deno.ns.d.ts`, ts.ScriptTarget.ESNext); +host.getSourceFile(`${ASSETS}/lib.deno.window.d.ts`, ts.ScriptTarget.ESNext); +host.getSourceFile(`${ASSETS}/lib.deno.worker.d.ts`, ts.ScriptTarget.ESNext); +host.getSourceFile( + `${ASSETS}/lib.deno.shared_globals.d.ts`, + ts.ScriptTarget.ESNext +); + +/** + * This function spins up TS compiler and loads all available libraries + * into memory (including ones specified above). + * + * Used to generate the foundational AST for all other compilations, so it can + * be cached as part of the snapshot and available to speed up startup. + */ +export const TS_SNAPSHOT_PROGRAM = ts.createProgram({ + rootNames: [`${ASSETS}/bootstrap.ts`], + options, + host +}); + +/** A module loader which is concatenated into bundle files. + * + * We read all static assets during the snapshotting process, which is + * why this is located in compiler_bootstrap. + */ +export const SYSTEM_LOADER = getAsset("system_loader.js"); diff --git a/cli/js/compiler/bundler.ts b/cli/js/compiler/bundler.ts new file mode 100644 index 000000000..ab987a7fc --- /dev/null +++ b/cli/js/compiler/bundler.ts @@ -0,0 +1,103 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { SYSTEM_LOADER } from "./bootstrap.ts"; +import { commonPath, normalizeString, CHAR_FORWARD_SLASH } from "./util.ts"; +import { assert } from "../util.ts"; + +/** Local state of what the root exports are of a root module. */ +let rootExports: 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[] +): string { + // 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: string; + if (rootExports && rootExports.length) { + instantiate = hasTla + ? `const __exp = await __instantiateAsync("${rootName}");\n` + : `const __exp = __instantiate("${rootName}");\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 __instantiateAsync("${rootName}");\n` + : `__instantiate("${rootName}");\n`; + } + return `${SYSTEM_LOADER}\n${data}\n${instantiate}`; +} + +/** Set the rootExports which will by the `emitBundle()` */ +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(); + // 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()); +} diff --git a/cli/js/compiler/host.ts b/cli/js/compiler/host.ts new file mode 100644 index 000000000..8032d83b3 --- /dev/null +++ b/cli/js/compiler/host.ts @@ -0,0 +1,329 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { ASSETS, MediaType, SourceFile } from "./sourcefile.ts"; +import { OUT_DIR, WriteFileCallback, getAsset } from "./util.ts"; +import { cwd } from "../ops/fs/dir.ts"; +import { assert, notImplemented } from "../util.ts"; +import * as util from "../util.ts"; + +/** Specifies the target that the host should use to inform the TypeScript + * compiler of what types should be used to validate the program against. */ +export enum CompilerHostTarget { + /** The main isolate library, where the main program runs. */ + Main = "main", + /** The runtime API library. */ + Runtime = "runtime", + /** The worker isolate library, where worker programs run. */ + Worker = "worker" +} + +export interface CompilerHostOptions { + /** Flag determines if the host should assume a single bundle output. */ + bundle?: boolean; + + /** Determines what the default library that should be used when type checking + * TS code. */ + target: CompilerHostTarget; + + /** A function to be used when the program emit occurs to write out files. */ + writeFile: WriteFileCallback; +} + +export interface ConfigureResponse { + ignoredOptions?: string[]; + diagnostics?: ts.Diagnostic[]; +} + +/** Options that need to be used when generating a bundle (either trusted or + * runtime). */ +export const defaultBundlerOptions: ts.CompilerOptions = { + 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 +}; + +/** Default options used by the compiler Host when compiling. */ +export const defaultCompileOptions: ts.CompilerOptions = { + allowJs: false, + allowNonTsExtensions: true, + checkJs: false, + esModuleInterop: true, + jsx: ts.JsxEmit.React, + module: ts.ModuleKind.ESNext, + outDir: OUT_DIR, + resolveJsonModule: true, + sourceMap: true, + strict: true, + stripComments: true, + target: ts.ScriptTarget.ESNext +}; + +/** 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", + "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 _target: CompilerHostTarget; + + private _writeFile: WriteFileCallback; + + private _getAsset(filename: string): SourceFile { + const lastSegment = filename.split("/").pop()!; + const url = ts.libMap.has(lastSegment) + ? ts.libMap.get(lastSegment)! + : lastSegment; + const sourceFile = SourceFile.get(url); + if (sourceFile) { + return sourceFile; + } + const name = url.includes(".") ? url : `${url}.d.ts`; + const sourceCode = getAsset(name); + return new SourceFile({ + url, + filename: `${ASSETS}/${name}`, + mediaType: MediaType.TypeScript, + sourceCode + }); + } + + /* Deno specific APIs */ + + /** Provides the `ts.HostCompiler` interface for Deno. */ + constructor({ bundle = false, target, writeFile }: CompilerHostOptions) { + this._target = target; + 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 { + util.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(): 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) { + assert(sourceFile.sourceCode != null); + sourceFile.tsSourceFile = ts.createSourceFile( + fileName.startsWith(ASSETS) ? sourceFile.filename : fileName, + sourceFile.sourceCode, + languageVersion + ); + delete sourceFile.sourceCode; + } + 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<ts.ResolvedModuleFull | undefined> { + 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 000000000..077303b61 --- /dev/null +++ b/cli/js/compiler/imports.ts @@ -0,0 +1,183 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { MediaType, SourceFile, SourceFileJson } from "./sourcefile.ts"; +import { normalizeString, CHAR_FORWARD_SLASH } from "./util.ts"; +import { cwd } from "../ops/fs/dir.ts"; +import { assert } from "../util.ts"; +import * as util from "../util.ts"; +import * as compilerOps from "../ops/compiler.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) === 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 = normalizeString( + resolvedPath, + !resolvedAbsolute, + "/", + code => code === 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 compilerOps.resolveModules(specifiers, referrer); +} + +/** Ops to Rust to fetch modules meta data. */ +function fetchSourceFiles( + specifiers: string[], + referrer?: string +): Promise<SourceFileJson[]> { + util.log("compiler_imports::fetchSourceFiles", { specifiers, referrer }); + return compilerOps.fetchSourceFiles(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<string, string>, + specifiers: Array<[string, string]>, + referrer?: string, + processJsImports = false +): 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(processJsImports), + sourceFile.url, + processJsImports + ); + } + } + 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, + processJsImports = false +): Promise<string[]> { + 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(processJsImports), + sourceFile.url, + processJsImports + ); + } + } + return resolvedSources; +} diff --git a/cli/js/compiler/sourcefile.ts b/cli/js/compiler/sourcefile.ts new file mode 100644 index 000000000..cfa09cde3 --- /dev/null +++ b/cli/js/compiler/sourcefile.ts @@ -0,0 +1,189 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { getMappedModuleName, parseTypeDirectives } from "./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; +} + +export const ASSETS = "$asset$"; + +/** 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 for "${fileName}" with mediaType "${MediaType[mediaType]}".` + ); + } +} + +/** 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(processJsImports: boolean): 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, + this.mediaType === MediaType.JavaScript || + this.mediaType === MediaType.JSX + ); + this.processed = true; + const files = (this.importedFiles = [] as Array<[string, string]>); + + function process(references: Array<{ fileName: string }>): 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 if ( + !( + !processJsImports && + (this.mediaType === MediaType.JavaScript || + this.mediaType === MediaType.JSX) + ) + ) { + process(importedFiles); + } + process(referencedFiles); + // built in libs comes across as `"dom"` for example, and should be filtered + // out during pre-processing as they are either already cached or they will + // be lazily fetched by the compiler host. Ones that contain full files are + // not filtered out and will be fetched as normal. + process( + libReferenceDirectives.filter( + ({ fileName }) => !ts.libMap.has(fileName.toLowerCase()) + ) + ); + process(typeReferenceDirectives); + return files; + } + + /** A cache of all the source files which have been loaded indexed by the + * url. */ + private static _moduleCache: Map<string, SourceFile> = 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<string, SourceFile> + > = 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/compiler/ts_global.d.ts b/cli/js/compiler/ts_global.d.ts new file mode 100644 index 000000000..7b9d84c7a --- /dev/null +++ b/cli/js/compiler/ts_global.d.ts @@ -0,0 +1,26 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This scopes the `ts` namespace globally, which is where it exists at runtime +// when building Deno, but the `typescript/lib/typescript.d.ts` is defined as a +// module. + +// Warning! This is a magical import. We don't want to have multiple copies of +// typescript.d.ts around the repo, there's already one in +// deno_typescript/typescript/lib/typescript.d.ts. Ideally we could simply point +// to that in this import specifier, but "cargo package" is very strict and +// requires all files to be present in a crate's subtree. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import * as ts_ from "$asset$/typescript.d.ts"; + +declare global { + namespace ts { + export = ts_; + } + + namespace ts { + // this are marked @internal in TypeScript, but we need to access them, + // there is a risk these could change in future versions of TypeScript + export const libs: string[]; + export const libMap: Map<string, string>; + } +} diff --git a/cli/js/compiler/type_directives.ts b/cli/js/compiler/type_directives.ts new file mode 100644 index 000000000..0f4ce932c --- /dev/null +++ b/cli/js/compiler/type_directives.ts @@ -0,0 +1,91 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +interface FileReference { + fileName: string; + pos: number; + end: number; +} + +/** Remap the module name based on any supplied type directives passed. */ +export function getMappedModuleName( + source: FileReference, + typeDirectives: Map<FileReference, string> +): string { + const { fileName: sourceFileName, pos: sourcePos } = source; + for (const [{ fileName, pos }, value] of typeDirectives.entries()) { + if (sourceFileName === fileName && sourcePos === pos) { + return value; + } + } + return source.fileName; +} + +/** Matches directives that look something like this and parses out the value + * of the directive: + * + * // @deno-types="./foo.d.ts" + * + * [See Diagram](http://bit.ly/31nZPCF) + */ +const typeDirectiveRegEx = /@deno-types\s*=\s*(["'])((?:(?=(\\?))\3.)*?)\1/gi; + +/** Matches `import`, `import from` or `export from` statements and parses out the value of the + * module specifier in the second capture group: + * + * import "./foo.js" + * import * as foo from "./foo.js" + * export { a, b, c } from "./bar.js" + * + * [See Diagram](http://bit.ly/2lOsp0K) + */ +const importExportRegEx = /(?:import|export)(?:\s+|\s+[\s\S]*?from\s+)?(["'])((?:(?=(\\?))\3.)*?)\1/; + +/** Parses out any Deno type directives that are part of the source code, or + * returns `undefined` if there are not any. + */ +export function parseTypeDirectives( + sourceCode: string | undefined +): Map<FileReference, string> | undefined { + if (!sourceCode) { + return; + } + + // collect all the directives in the file and their start and end positions + const directives: FileReference[] = []; + let maybeMatch: RegExpExecArray | null = null; + while ((maybeMatch = typeDirectiveRegEx.exec(sourceCode))) { + const [matchString, , fileName] = maybeMatch; + const { index: pos } = maybeMatch; + directives.push({ + fileName, + pos, + end: pos + matchString.length + }); + } + if (!directives.length) { + return; + } + + // work from the last directive backwards for the next `import`/`export` + // statement + directives.reverse(); + const results = new Map<FileReference, string>(); + for (const { end, fileName, pos } of directives) { + const searchString = sourceCode.substring(end); + const maybeMatch = importExportRegEx.exec(searchString); + if (maybeMatch) { + const [matchString, , targetFileName] = maybeMatch; + const targetPos = + end + maybeMatch.index + matchString.indexOf(targetFileName) - 1; + const target: FileReference = { + fileName: targetFileName, + pos: targetPos, + end: targetPos + targetFileName.length + }; + results.set(target, fileName); + } + sourceCode = sourceCode.substring(0, pos); + } + + return results; +} diff --git a/cli/js/compiler/util.ts b/cli/js/compiler/util.ts new file mode 100644 index 000000000..c1afbd581 --- /dev/null +++ b/cli/js/compiler/util.ts @@ -0,0 +1,448 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { bold, cyan, yellow } from "../colors.ts"; +import { CompilerOptions } from "./api.ts"; +import { buildBundle } from "./bundler.ts"; +import { ConfigureResponse, Host } from "./host.ts"; +import { SourceFile } from "./sourcefile.ts"; +import { atob, TextEncoder } from "../web/text_encoding.ts"; +import * as compilerOps from "../ops/compiler.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<string, string>; + emitBundle?: string; + sources?: Record<string, string>; +} + +// 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 + compilerOps.cache(".map", moduleId, contents); + } else if ( + emittedFileName.endsWith(".js") || + emittedFileName.endsWith(".json") + ) { + // Compiled JavaScript + compilerOps.cache(".js", moduleId, contents); + } else { + assert(false, `Trying to cache unhandled file type "${emittedFileName}"`); + } +} + +/** Retrieve an asset from Rust. */ +export function getAsset(name: string): string { + return compilerOps.getAsset(name); +} + +/** 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<string, unknown> = {}; + const keys = Object.keys(options) as Array<keyof CompilerOptions>; + 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; +} |