summaryrefslogtreecommitdiff
path: root/cli/js/compiler
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/compiler')
-rw-r--r--cli/js/compiler/api.ts409
-rw-r--r--cli/js/compiler/bootstrap.ts52
-rw-r--r--cli/js/compiler/bundler.ts103
-rw-r--r--cli/js/compiler/host.ts329
-rw-r--r--cli/js/compiler/imports.ts183
-rw-r--r--cli/js/compiler/sourcefile.ts189
-rw-r--r--cli/js/compiler/ts_global.d.ts26
-rw-r--r--cli/js/compiler/type_directives.ts91
-rw-r--r--cli/js/compiler/util.ts448
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;
+}