From 99a0c6df79b903e4fe72ce066787039bdede3868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Wed, 11 Mar 2020 10:53:06 +0100 Subject: 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/" --- cli/js/compiler.ts | 14 +- cli/js/compiler/api.ts | 409 +++++++++++++++++++ cli/js/compiler/bootstrap.ts | 52 +++ cli/js/compiler/bundler.ts | 103 +++++ cli/js/compiler/host.ts | 329 ++++++++++++++++ cli/js/compiler/imports.ts | 183 +++++++++ cli/js/compiler/sourcefile.ts | 189 +++++++++ cli/js/compiler/ts_global.d.ts | 26 ++ cli/js/compiler/type_directives.ts | 91 +++++ cli/js/compiler/util.ts | 448 +++++++++++++++++++++ cli/js/compiler_api.ts | 409 ------------------- cli/js/compiler_bootstrap.ts | 52 --- cli/js/compiler_bundler.ts | 107 ----- cli/js/compiler_host.ts | 329 ---------------- cli/js/compiler_imports.ts | 187 --------- cli/js/compiler_sourcefile.ts | 192 --------- cli/js/compiler_type_directives.ts | 91 ----- cli/js/compiler_util.ts | 448 --------------------- cli/js/console.ts | 776 ------------------------------------- cli/js/console_table.ts | 94 ----- cli/js/deno.ts | 4 +- cli/js/globals.ts | 8 +- cli/js/mock_builtin.js | 2 - cli/js/performance.ts | 16 - cli/js/repl.ts | 2 +- cli/js/symbols.ts | 2 +- cli/js/testing.ts | 2 +- cli/js/timers.ts | 315 --------------- cli/js/ts_global.d.ts | 26 -- cli/js/web/README.md | 8 + cli/js/web/console.ts | 776 +++++++++++++++++++++++++++++++++++++ cli/js/web/console_table.ts | 94 +++++ cli/js/web/headers.ts | 2 +- cli/js/web/performance.ts | 16 + cli/js/web/timers.ts | 315 +++++++++++++++ cli/js/web/url.ts | 2 +- cli/js/web/workers.ts | 170 ++++++++ cli/js/workers.ts | 170 -------- 38 files changed, 3227 insertions(+), 3232 deletions(-) create mode 100644 cli/js/compiler/api.ts create mode 100644 cli/js/compiler/bootstrap.ts create mode 100644 cli/js/compiler/bundler.ts create mode 100644 cli/js/compiler/host.ts create mode 100644 cli/js/compiler/imports.ts create mode 100644 cli/js/compiler/sourcefile.ts create mode 100644 cli/js/compiler/ts_global.d.ts create mode 100644 cli/js/compiler/type_directives.ts create mode 100644 cli/js/compiler/util.ts delete mode 100644 cli/js/compiler_api.ts delete mode 100644 cli/js/compiler_bootstrap.ts delete mode 100644 cli/js/compiler_bundler.ts delete mode 100644 cli/js/compiler_host.ts delete mode 100644 cli/js/compiler_imports.ts delete mode 100644 cli/js/compiler_sourcefile.ts delete mode 100644 cli/js/compiler_type_directives.ts delete mode 100644 cli/js/compiler_util.ts delete mode 100644 cli/js/console.ts delete mode 100644 cli/js/console_table.ts delete mode 100644 cli/js/mock_builtin.js delete mode 100644 cli/js/performance.ts delete mode 100644 cli/js/timers.ts delete mode 100644 cli/js/ts_global.d.ts create mode 100644 cli/js/web/README.md create mode 100644 cli/js/web/console.ts create mode 100644 cli/js/web/console_table.ts create mode 100644 cli/js/web/performance.ts create mode 100644 cli/js/web/timers.ts create mode 100644 cli/js/web/workers.ts delete mode 100644 cli/js/workers.ts (limited to 'cli/js') diff --git a/cli/js/compiler.ts b/cli/js/compiler.ts index 80ea16fb0..ed156ef0a 100644 --- a/cli/js/compiler.ts +++ b/cli/js/compiler.ts @@ -11,23 +11,23 @@ // to properly setup runtime. // NOTE: this import has side effects! -import "./ts_global.d.ts"; +import "./compiler/ts_global.d.ts"; -import { TranspileOnlyResult } from "./compiler_api.ts"; -import { TS_SNAPSHOT_PROGRAM } from "./compiler_bootstrap.ts"; -import { setRootExports } from "./compiler_bundler.ts"; +import { TranspileOnlyResult } from "./compiler/api.ts"; +import { TS_SNAPSHOT_PROGRAM } from "./compiler/bootstrap.ts"; +import { setRootExports } from "./compiler/bundler.ts"; import { CompilerHostTarget, defaultBundlerOptions, defaultRuntimeCompileOptions, defaultTranspileOptions, Host -} from "./compiler_host.ts"; +} from "./compiler/host.ts"; import { processImports, processLocalImports, resolveModules -} from "./compiler_imports.ts"; +} from "./compiler/imports.ts"; import { createWriteFile, CompilerRequestType, @@ -36,7 +36,7 @@ import { WriteFileState, processConfigureResponse, base64ToUint8Array -} from "./compiler_util.ts"; +} from "./compiler/util.ts"; import { Diagnostic, DiagnosticItem } from "./diagnostics.ts"; import { fromTypeScriptDiagnostic } from "./diagnostics_util.ts"; import { assert } from "./util.ts"; 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; + + /** 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, + options: CompilerOptions = {} +): Promise> { + 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, + options: CompilerOptions = {} +): Promise<[DiagnosticItem[] | undefined, Record]> { + 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, + 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 { + 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 { + 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, + 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 { + 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 = 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 + > = 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; + } +} 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 +): 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 | 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(); + 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; + emitBundle?: string; + sources?: Record; +} + +// Warning! The values in this enum are duplicated in `cli/msg.rs` +// Update carefully! +export enum CompilerRequestType { + Compile = 0, + RuntimeCompile = 1, + RuntimeTranspile = 2 +} + +export const OUT_DIR = "$deno$"; + +/** Cache the contents of a file on the trusted side. */ +function cache( + moduleId: string, + emittedFileName: string, + contents: string, + checkJs = false +): void { + util.log("compiler::cache", { moduleId, emittedFileName, checkJs }); + const sf = SourceFile.get(moduleId); + + if (sf) { + // NOTE: If it's a `.json` file we don't want to write it to disk. + // JSON files are loaded and used by TS compiler to check types, but we don't want + // to emit them to disk because output file is the same as input file. + if (sf.extension === ts.Extension.Json) { + return; + } + + // NOTE: JavaScript files are only cached to disk if `checkJs` + // option in on + if (sf.extension === ts.Extension.Js && !checkJs) { + return; + } + } + + if (emittedFileName.endsWith(".map")) { + // Source Map + 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 = {}; + const keys = Object.keys(options) as Array; + const files: string[] = []; + for (const key of keys) { + switch (key) { + case "jsx": + const value = options[key]; + if (value === "preserve") { + out[key] = ts.JsxEmit.Preserve; + } else if (value === "react") { + out[key] = ts.JsxEmit.React; + } else { + out[key] = ts.JsxEmit.ReactNative; + } + break; + case "module": + switch (options[key]) { + case "amd": + out[key] = ts.ModuleKind.AMD; + break; + case "commonjs": + out[key] = ts.ModuleKind.CommonJS; + break; + case "es2015": + case "es6": + out[key] = ts.ModuleKind.ES2015; + break; + case "esnext": + out[key] = ts.ModuleKind.ESNext; + break; + case "none": + out[key] = ts.ModuleKind.None; + break; + case "system": + out[key] = ts.ModuleKind.System; + break; + case "umd": + out[key] = ts.ModuleKind.UMD; + break; + default: + throw new TypeError("Unexpected module type"); + } + break; + case "target": + switch (options[key]) { + case "es3": + out[key] = ts.ScriptTarget.ES3; + break; + case "es5": + out[key] = ts.ScriptTarget.ES5; + break; + case "es6": + case "es2015": + out[key] = ts.ScriptTarget.ES2015; + break; + case "es2016": + out[key] = ts.ScriptTarget.ES2016; + break; + case "es2017": + out[key] = ts.ScriptTarget.ES2017; + break; + case "es2018": + out[key] = ts.ScriptTarget.ES2018; + break; + case "es2019": + out[key] = ts.ScriptTarget.ES2019; + break; + case "es2020": + out[key] = ts.ScriptTarget.ES2020; + break; + case "esnext": + out[key] = ts.ScriptTarget.ESNext; + break; + default: + throw new TypeError("Unexpected emit target."); + } + break; + case "types": + const types = options[key]; + assert(types); + files.push(...types); + break; + default: + out[key] = options[key]; + } + } + return { + options: out as ts.CompilerOptions, + files: files.length ? files : undefined + }; +} + +/** An array of TypeScript diagnostic types we ignore. */ +export const ignoredDiagnostics = [ + // TS2306: File 'file:///Users/rld/src/deno/cli/tests/subdir/amd_like.js' is + // not a module. + 2306, + // TS1375: 'await' expressions are only allowed at the top level of a file + // when that file is a module, but this file has no imports or exports. + // Consider adding an empty 'export {}' to make this file a module. + 1375, + // TS1103: 'for-await-of' statement is only allowed within an async function + // or async generator. + 1103, + // TS2691: An import path cannot end with a '.ts' extension. Consider + // importing 'bad-module' instead. + 2691, + // TS5009: Cannot find the common subdirectory path for the input files. + 5009, + // TS5055: Cannot write file + // 'http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js' + // because it would overwrite input file. + 5055, + // TypeScript is overly opinionated that only CommonJS modules kinds can + // support JSON imports. Allegedly this was fixed in + // Microsoft/TypeScript#26825 but that doesn't seem to be working here, + // so we will ignore complaints about this compiler setting. + 5070, + // TS7016: Could not find a declaration file for module '...'. '...' + // implicitly has an 'any' type. This is due to `allowJs` being off by + // default but importing of a JavaScript module. + 7016 +]; + +/** When doing a host configuration, processing the response and logging out + * and options which were ignored. */ +export function processConfigureResponse( + configResult: ConfigureResponse, + configPath: string +): ts.Diagnostic[] | undefined { + const { ignoredOptions, diagnostics } = configResult; + if (ignoredOptions) { + console.warn( + yellow(`Unsupported compiler options in "${configPath}"\n`) + + cyan(` The following options were ignored:\n`) + + ` ${ignoredOptions.map((value): string => bold(value)).join(", ")}` + ); + } + return diagnostics; +} + +// Constants used by `normalizeString` and `resolvePath` +export const CHAR_DOT = 46; /* . */ +export const CHAR_FORWARD_SLASH = 47; /* / */ + +/** Resolves `.` and `..` elements in a path with directory names */ +export function normalizeString( + path: string, + allowAboveRoot: boolean, + separator: string, + isPathSeparator: (code: number) => boolean +): string { + let res = ""; + let lastSegmentLength = 0; + let lastSlash = -1; + let dots = 0; + let code: number; + for (let i = 0, len = path.length; i <= len; ++i) { + if (i < len) code = path.charCodeAt(i); + else if (isPathSeparator(code!)) break; + else code = CHAR_FORWARD_SLASH; + + if (isPathSeparator(code)) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if ( + res.length < 2 || + lastSegmentLength !== 2 || + res.charCodeAt(res.length - 1) !== CHAR_DOT || + res.charCodeAt(res.length - 2) !== CHAR_DOT + ) { + if (res.length > 2) { + const lastSlashIndex = res.lastIndexOf(separator); + if (lastSlashIndex === -1) { + res = ""; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); + } + lastSlash = i; + dots = 0; + continue; + } else if (res.length === 2 || res.length === 1) { + res = ""; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) res += `${separator}..`; + else res = ".."; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); + else res = path.slice(lastSlash + 1, i); + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === CHAR_DOT && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} + +/** Return the common path shared by the `paths`. + * + * @param paths The set of paths to compare. + * @param sep An optional separator to use. Defaults to `/`. + * @internal + */ +export function commonPath(paths: string[], sep = "/"): string { + const [first = "", ...remaining] = paths; + if (first === "" || remaining.length === 0) { + return first.substring(0, first.lastIndexOf(sep) + 1); + } + const parts = first.split(sep); + + let endOfPrefix = parts.length; + for (const path of remaining) { + const compare = path.split(sep); + for (let i = 0; i < endOfPrefix; i++) { + if (compare[i] !== parts[i]) { + endOfPrefix = i; + } + } + + if (endOfPrefix === 0) { + return ""; + } + } + const prefix = parts.slice(0, endOfPrefix).join(sep); + return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`; +} + +/** Utility function to turn the number of bytes into a human readable + * unit */ +function humanFileSize(bytes: number): string { + const thresh = 1000; + if (Math.abs(bytes) < thresh) { + return bytes + " B"; + } + const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + let u = -1; + do { + bytes /= thresh; + ++u; + } while (Math.abs(bytes) >= thresh && u < units.length - 1); + return `${bytes.toFixed(1)} ${units[u]}`; +} + +// @internal +export function base64ToUint8Array(data: string): Uint8Array { + const binString = atob(data); + const size = binString.length; + const bytes = new Uint8Array(size); + for (let i = 0; i < size; i++) { + bytes[i] = binString.charCodeAt(i); + } + return bytes; +} diff --git a/cli/js/compiler_api.ts b/cli/js/compiler_api.ts deleted file mode 100644 index 8282f0717..000000000 --- a/cli/js/compiler_api.ts +++ /dev/null @@ -1,409 +0,0 @@ -// 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; - - /** 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, - options: CompilerOptions = {} -): Promise> { - 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, - options: CompilerOptions = {} -): Promise<[DiagnosticItem[] | undefined, Record]> { - 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, - 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 deleted file mode 100644 index c502e2e01..000000000 --- a/cli/js/compiler_bootstrap.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -import { CompilerHostTarget, Host } from "./compiler_host.ts"; -import { ASSETS } from "./compiler_sourcefile.ts"; -import { getAsset } from "./compiler_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 deleted file mode 100644 index 3a9d3a212..000000000 --- a/cli/js/compiler_bundler.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -import { SYSTEM_LOADER } from "./compiler_bootstrap.ts"; -import { - commonPath, - normalizeString, - CHAR_FORWARD_SLASH -} from "./compiler_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 deleted file mode 100644 index 413ffa6e1..000000000 --- a/cli/js/compiler_host.ts +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -import { ASSETS, MediaType, SourceFile } from "./compiler_sourcefile.ts"; -import { OUT_DIR, WriteFileCallback, getAsset } from "./compiler_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 { - 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 deleted file mode 100644 index a5f3cd17e..000000000 --- a/cli/js/compiler_imports.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -import { - MediaType, - SourceFile, - SourceFileJson -} from "./compiler_sourcefile.ts"; -import { normalizeString, CHAR_FORWARD_SLASH } from "./compiler_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 { - 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, - 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 { - 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 deleted file mode 100644 index cc7d4aa3e..000000000 --- a/cli/js/compiler_sourcefile.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -import { - getMappedModuleName, - parseTypeDirectives -} from "./compiler_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 = 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 - > = 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_type_directives.ts b/cli/js/compiler_type_directives.ts deleted file mode 100644 index 0f4ce932c..000000000 --- a/cli/js/compiler_type_directives.ts +++ /dev/null @@ -1,91 +0,0 @@ -// 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 -): 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 | 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(); - 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 deleted file mode 100644 index 3cc661d6c..000000000 --- a/cli/js/compiler_util.ts +++ /dev/null @@ -1,448 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -import { bold, cyan, yellow } from "./colors.ts"; -import { CompilerOptions } from "./compiler_api.ts"; -import { buildBundle } from "./compiler_bundler.ts"; -import { ConfigureResponse, Host } from "./compiler_host.ts"; -import { SourceFile } from "./compiler_sourcefile.ts"; -import { 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; - emitBundle?: string; - sources?: Record; -} - -// Warning! The values in this enum are duplicated in `cli/msg.rs` -// Update carefully! -export enum CompilerRequestType { - Compile = 0, - RuntimeCompile = 1, - RuntimeTranspile = 2 -} - -export const OUT_DIR = "$deno$"; - -/** Cache the contents of a file on the trusted side. */ -function cache( - moduleId: string, - emittedFileName: string, - contents: string, - checkJs = false -): void { - util.log("compiler::cache", { moduleId, emittedFileName, checkJs }); - const sf = SourceFile.get(moduleId); - - if (sf) { - // NOTE: If it's a `.json` file we don't want to write it to disk. - // JSON files are loaded and used by TS compiler to check types, but we don't want - // to emit them to disk because output file is the same as input file. - if (sf.extension === ts.Extension.Json) { - return; - } - - // NOTE: JavaScript files are only cached to disk if `checkJs` - // option in on - if (sf.extension === ts.Extension.Js && !checkJs) { - return; - } - } - - if (emittedFileName.endsWith(".map")) { - // Source Map - 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 = {}; - const keys = Object.keys(options) as Array; - const files: string[] = []; - for (const key of keys) { - switch (key) { - case "jsx": - const value = options[key]; - if (value === "preserve") { - out[key] = ts.JsxEmit.Preserve; - } else if (value === "react") { - out[key] = ts.JsxEmit.React; - } else { - out[key] = ts.JsxEmit.ReactNative; - } - break; - case "module": - switch (options[key]) { - case "amd": - out[key] = ts.ModuleKind.AMD; - break; - case "commonjs": - out[key] = ts.ModuleKind.CommonJS; - break; - case "es2015": - case "es6": - out[key] = ts.ModuleKind.ES2015; - break; - case "esnext": - out[key] = ts.ModuleKind.ESNext; - break; - case "none": - out[key] = ts.ModuleKind.None; - break; - case "system": - out[key] = ts.ModuleKind.System; - break; - case "umd": - out[key] = ts.ModuleKind.UMD; - break; - default: - throw new TypeError("Unexpected module type"); - } - break; - case "target": - switch (options[key]) { - case "es3": - out[key] = ts.ScriptTarget.ES3; - break; - case "es5": - out[key] = ts.ScriptTarget.ES5; - break; - case "es6": - case "es2015": - out[key] = ts.ScriptTarget.ES2015; - break; - case "es2016": - out[key] = ts.ScriptTarget.ES2016; - break; - case "es2017": - out[key] = ts.ScriptTarget.ES2017; - break; - case "es2018": - out[key] = ts.ScriptTarget.ES2018; - break; - case "es2019": - out[key] = ts.ScriptTarget.ES2019; - break; - case "es2020": - out[key] = ts.ScriptTarget.ES2020; - break; - case "esnext": - out[key] = ts.ScriptTarget.ESNext; - break; - default: - throw new TypeError("Unexpected emit target."); - } - break; - case "types": - const types = options[key]; - assert(types); - files.push(...types); - break; - default: - out[key] = options[key]; - } - } - return { - options: out as ts.CompilerOptions, - files: files.length ? files : undefined - }; -} - -/** An array of TypeScript diagnostic types we ignore. */ -export const ignoredDiagnostics = [ - // TS2306: File 'file:///Users/rld/src/deno/cli/tests/subdir/amd_like.js' is - // not a module. - 2306, - // TS1375: 'await' expressions are only allowed at the top level of a file - // when that file is a module, but this file has no imports or exports. - // Consider adding an empty 'export {}' to make this file a module. - 1375, - // TS1103: 'for-await-of' statement is only allowed within an async function - // or async generator. - 1103, - // TS2691: An import path cannot end with a '.ts' extension. Consider - // importing 'bad-module' instead. - 2691, - // TS5009: Cannot find the common subdirectory path for the input files. - 5009, - // TS5055: Cannot write file - // 'http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js' - // because it would overwrite input file. - 5055, - // TypeScript is overly opinionated that only CommonJS modules kinds can - // support JSON imports. Allegedly this was fixed in - // Microsoft/TypeScript#26825 but that doesn't seem to be working here, - // so we will ignore complaints about this compiler setting. - 5070, - // TS7016: Could not find a declaration file for module '...'. '...' - // implicitly has an 'any' type. This is due to `allowJs` being off by - // default but importing of a JavaScript module. - 7016 -]; - -/** When doing a host configuration, processing the response and logging out - * and options which were ignored. */ -export function processConfigureResponse( - configResult: ConfigureResponse, - configPath: string -): ts.Diagnostic[] | undefined { - const { ignoredOptions, diagnostics } = configResult; - if (ignoredOptions) { - console.warn( - yellow(`Unsupported compiler options in "${configPath}"\n`) + - cyan(` The following options were ignored:\n`) + - ` ${ignoredOptions.map((value): string => bold(value)).join(", ")}` - ); - } - return diagnostics; -} - -// Constants used by `normalizeString` and `resolvePath` -export const CHAR_DOT = 46; /* . */ -export const CHAR_FORWARD_SLASH = 47; /* / */ - -/** Resolves `.` and `..` elements in a path with directory names */ -export function normalizeString( - path: string, - allowAboveRoot: boolean, - separator: string, - isPathSeparator: (code: number) => boolean -): string { - let res = ""; - let lastSegmentLength = 0; - let lastSlash = -1; - let dots = 0; - let code: number; - for (let i = 0, len = path.length; i <= len; ++i) { - if (i < len) code = path.charCodeAt(i); - else if (isPathSeparator(code!)) break; - else code = CHAR_FORWARD_SLASH; - - if (isPathSeparator(code)) { - if (lastSlash === i - 1 || dots === 1) { - // NOOP - } else if (lastSlash !== i - 1 && dots === 2) { - if ( - res.length < 2 || - lastSegmentLength !== 2 || - res.charCodeAt(res.length - 1) !== CHAR_DOT || - res.charCodeAt(res.length - 2) !== CHAR_DOT - ) { - if (res.length > 2) { - const lastSlashIndex = res.lastIndexOf(separator); - if (lastSlashIndex === -1) { - res = ""; - lastSegmentLength = 0; - } else { - res = res.slice(0, lastSlashIndex); - lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); - } - lastSlash = i; - dots = 0; - continue; - } else if (res.length === 2 || res.length === 1) { - res = ""; - lastSegmentLength = 0; - lastSlash = i; - dots = 0; - continue; - } - } - if (allowAboveRoot) { - if (res.length > 0) res += `${separator}..`; - else res = ".."; - lastSegmentLength = 2; - } - } else { - if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); - else res = path.slice(lastSlash + 1, i); - lastSegmentLength = i - lastSlash - 1; - } - lastSlash = i; - dots = 0; - } else if (code === CHAR_DOT && dots !== -1) { - ++dots; - } else { - dots = -1; - } - } - return res; -} - -/** Return the common path shared by the `paths`. - * - * @param paths The set of paths to compare. - * @param sep An optional separator to use. Defaults to `/`. - * @internal - */ -export function commonPath(paths: string[], sep = "/"): string { - const [first = "", ...remaining] = paths; - if (first === "" || remaining.length === 0) { - return first.substring(0, first.lastIndexOf(sep) + 1); - } - const parts = first.split(sep); - - let endOfPrefix = parts.length; - for (const path of remaining) { - const compare = path.split(sep); - for (let i = 0; i < endOfPrefix; i++) { - if (compare[i] !== parts[i]) { - endOfPrefix = i; - } - } - - if (endOfPrefix === 0) { - return ""; - } - } - const prefix = parts.slice(0, endOfPrefix).join(sep); - return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`; -} - -/** Utility function to turn the number of bytes into a human readable - * unit */ -function humanFileSize(bytes: number): string { - const thresh = 1000; - if (Math.abs(bytes) < thresh) { - return bytes + " B"; - } - const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - let u = -1; - do { - bytes /= thresh; - ++u; - } while (Math.abs(bytes) >= thresh && u < units.length - 1); - return `${bytes.toFixed(1)} ${units[u]}`; -} - -// @internal -export function base64ToUint8Array(data: string): Uint8Array { - const binString = atob(data); - const size = binString.length; - const bytes = new Uint8Array(size); - for (let i = 0; i < size; i++) { - bytes[i] = binString.charCodeAt(i); - } - return bytes; -} diff --git a/cli/js/console.ts b/cli/js/console.ts deleted file mode 100644 index 60329ab00..000000000 --- a/cli/js/console.ts +++ /dev/null @@ -1,776 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { isTypedArray } from "./util.ts"; -import { TypedArray } from "./types.ts"; -import { TextEncoder } from "./web/text_encoding.ts"; -import { File, stdout } from "./files.ts"; -import { cliTable } from "./console_table.ts"; -import { exposeForTest } from "./internals.ts"; - -type ConsoleContext = Set; -type ConsoleOptions = Partial<{ - showHidden: boolean; - depth: number; - colors: boolean; - indentLevel: number; -}>; - -// Default depth of logging nested objects -const DEFAULT_MAX_DEPTH = 4; - -// Number of elements an object must have before it's displayed in appreviated -// form. -const OBJ_ABBREVIATE_SIZE = 5; - -const STR_ABBREVIATE_SIZE = 100; - -// Char codes -const CHAR_PERCENT = 37; /* % */ -const CHAR_LOWERCASE_S = 115; /* s */ -const CHAR_LOWERCASE_D = 100; /* d */ -const CHAR_LOWERCASE_I = 105; /* i */ -const CHAR_LOWERCASE_F = 102; /* f */ -const CHAR_LOWERCASE_O = 111; /* o */ -const CHAR_UPPERCASE_O = 79; /* O */ -const CHAR_LOWERCASE_C = 99; /* c */ -export class CSI { - static kClear = "\x1b[1;1H"; - static kClearScreenDown = "\x1b[0J"; -} - -/* eslint-disable @typescript-eslint/no-use-before-define */ - -function cursorTo(stream: File, _x: number, _y?: number): void { - const uint8 = new TextEncoder().encode(CSI.kClear); - stream.writeSync(uint8); -} - -function clearScreenDown(stream: File): void { - const uint8 = new TextEncoder().encode(CSI.kClearScreenDown); - stream.writeSync(uint8); -} - -function getClassInstanceName(instance: unknown): string { - if (typeof instance !== "object") { - return ""; - } - if (!instance) { - return ""; - } - - const proto = Object.getPrototypeOf(instance); - if (proto && proto.constructor) { - return proto.constructor.name; // could be "Object" or "Array" - } - - return ""; -} - -function createFunctionString(value: Function, _ctx: ConsoleContext): string { - // Might be Function/AsyncFunction/GeneratorFunction - const cstrName = Object.getPrototypeOf(value).constructor.name; - if (value.name && value.name !== "anonymous") { - // from MDN spec - return `[${cstrName}: ${value.name}]`; - } - return `[${cstrName}]`; -} - -interface IterablePrintConfig { - typeName: string; - displayName: string; - delims: [string, string]; - entryHandler: ( - entry: T, - ctx: ConsoleContext, - level: number, - maxLevel: number - ) => string; -} - -function createIterableString( - value: Iterable, - ctx: ConsoleContext, - level: number, - maxLevel: number, - config: IterablePrintConfig -): string { - if (level >= maxLevel) { - return `[${config.typeName}]`; - } - ctx.add(value); - - const entries: string[] = []; - // In cases e.g. Uint8Array.prototype - try { - for (const el of value) { - entries.push(config.entryHandler(el, ctx, level + 1, maxLevel)); - } - } catch (e) {} - ctx.delete(value); - const iPrefix = `${config.displayName ? config.displayName + " " : ""}`; - const iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; - return `${iPrefix}${config.delims[0]}${iContent}${config.delims[1]}`; -} - -function stringify( - value: unknown, - ctx: ConsoleContext, - level: number, - maxLevel: number -): string { - switch (typeof value) { - case "string": - return value; - case "number": - // Special handling of -0 - return Object.is(value, -0) ? "-0" : `${value}`; - case "boolean": - case "undefined": - case "symbol": - return String(value); - case "bigint": - return `${value}n`; - case "function": - return createFunctionString(value as Function, ctx); - case "object": - if (value === null) { - return "null"; - } - - if (ctx.has(value)) { - return "[Circular]"; - } - - return createObjectString(value, ctx, level, maxLevel); - default: - return "[Not Implemented]"; - } -} - -// Print strings when they are inside of arrays or objects with quotes -function stringifyWithQuotes( - value: unknown, - ctx: ConsoleContext, - level: number, - maxLevel: number -): string { - switch (typeof value) { - case "string": - const trunc = - value.length > STR_ABBREVIATE_SIZE - ? value.slice(0, STR_ABBREVIATE_SIZE) + "..." - : value; - return JSON.stringify(trunc); - default: - return stringify(value, ctx, level, maxLevel); - } -} - -function createArrayString( - value: unknown[], - ctx: ConsoleContext, - level: number, - maxLevel: number -): string { - const printConfig: IterablePrintConfig = { - typeName: "Array", - displayName: "", - delims: ["[", "]"], - entryHandler: (el, ctx, level, maxLevel): string => - stringifyWithQuotes(el, ctx, level + 1, maxLevel) - }; - return createIterableString(value, ctx, level, maxLevel, printConfig); -} - -function createTypedArrayString( - typedArrayName: string, - value: TypedArray, - ctx: ConsoleContext, - level: number, - maxLevel: number -): string { - const printConfig: IterablePrintConfig = { - typeName: typedArrayName, - displayName: typedArrayName, - delims: ["[", "]"], - entryHandler: (el, ctx, level, maxLevel): string => - stringifyWithQuotes(el, ctx, level + 1, maxLevel) - }; - return createIterableString(value, ctx, level, maxLevel, printConfig); -} - -function createSetString( - value: Set, - ctx: ConsoleContext, - level: number, - maxLevel: number -): string { - const printConfig: IterablePrintConfig = { - typeName: "Set", - displayName: "Set", - delims: ["{", "}"], - entryHandler: (el, ctx, level, maxLevel): string => - stringifyWithQuotes(el, ctx, level + 1, maxLevel) - }; - return createIterableString(value, ctx, level, maxLevel, printConfig); -} - -function createMapString( - value: Map, - ctx: ConsoleContext, - level: number, - maxLevel: number -): string { - const printConfig: IterablePrintConfig<[unknown, unknown]> = { - typeName: "Map", - displayName: "Map", - delims: ["{", "}"], - entryHandler: (el, ctx, level, maxLevel): string => { - const [key, val] = el; - return `${stringifyWithQuotes( - key, - ctx, - level + 1, - maxLevel - )} => ${stringifyWithQuotes(val, ctx, level + 1, maxLevel)}`; - } - }; - return createIterableString(value, ctx, level, maxLevel, printConfig); -} - -function createWeakSetString(): string { - return "WeakSet { [items unknown] }"; // as seen in Node -} - -function createWeakMapString(): string { - return "WeakMap { [items unknown] }"; // as seen in Node -} - -function createDateString(value: Date): string { - // without quotes, ISO format - return value.toISOString(); -} - -function createRegExpString(value: RegExp): string { - return value.toString(); -} - -/* eslint-disable @typescript-eslint/ban-types */ - -function createStringWrapperString(value: String): string { - return `[String: "${value.toString()}"]`; -} - -function createBooleanWrapperString(value: Boolean): string { - return `[Boolean: ${value.toString()}]`; -} - -function createNumberWrapperString(value: Number): string { - return `[Number: ${value.toString()}]`; -} - -/* eslint-enable @typescript-eslint/ban-types */ - -// TODO: Promise, requires v8 bindings to get info -// TODO: Proxy - -function createRawObjectString( - value: { [key: string]: unknown }, - ctx: ConsoleContext, - level: number, - maxLevel: number -): string { - if (level >= maxLevel) { - return "[Object]"; - } - ctx.add(value); - - let baseString = ""; - - const className = getClassInstanceName(value); - let shouldShowClassName = false; - if (className && className !== "Object" && className !== "anonymous") { - shouldShowClassName = true; - } - const keys = Object.keys(value); - const entries: string[] = keys.map((key): string => { - if (keys.length > OBJ_ABBREVIATE_SIZE) { - return key; - } else { - return `${key}: ${stringifyWithQuotes( - value[key], - ctx, - level + 1, - maxLevel - )}`; - } - }); - - ctx.delete(value); - - if (entries.length === 0) { - baseString = "{}"; - } else { - baseString = `{ ${entries.join(", ")} }`; - } - - if (shouldShowClassName) { - baseString = `${className} ${baseString}`; - } - - return baseString; -} - -function createObjectString( - value: {}, - ...args: [ConsoleContext, number, number] -): string { - if (customInspect in value && typeof value[customInspect] === "function") { - try { - return String(value[customInspect]!()); - } catch {} - } - if (value instanceof Error) { - return String(value.stack); - } else if (Array.isArray(value)) { - return createArrayString(value, ...args); - } else if (value instanceof Number) { - return createNumberWrapperString(value); - } else if (value instanceof Boolean) { - return createBooleanWrapperString(value); - } else if (value instanceof String) { - return createStringWrapperString(value); - } else if (value instanceof RegExp) { - return createRegExpString(value); - } else if (value instanceof Date) { - return createDateString(value); - } else if (value instanceof Set) { - return createSetString(value, ...args); - } else if (value instanceof Map) { - return createMapString(value, ...args); - } else if (value instanceof WeakSet) { - return createWeakSetString(); - } else if (value instanceof WeakMap) { - return createWeakMapString(); - } else if (isTypedArray(value)) { - return createTypedArrayString( - Object.getPrototypeOf(value).constructor.name, - value, - ...args - ); - } else { - // Otherwise, default object formatting - return createRawObjectString(value, ...args); - } -} - -/** @internal */ -export function stringifyArgs( - args: unknown[], - { depth = DEFAULT_MAX_DEPTH, indentLevel = 0 }: ConsoleOptions = {} -): string { - const first = args[0]; - let a = 0; - let str = ""; - let join = ""; - - if (typeof first === "string") { - let tempStr: string; - let lastPos = 0; - - for (let i = 0; i < first.length - 1; i++) { - if (first.charCodeAt(i) === CHAR_PERCENT) { - const nextChar = first.charCodeAt(++i); - if (a + 1 !== args.length) { - switch (nextChar) { - case CHAR_LOWERCASE_S: - // format as a string - tempStr = String(args[++a]); - break; - case CHAR_LOWERCASE_D: - case CHAR_LOWERCASE_I: - // format as an integer - const tempInteger = args[++a]; - if (typeof tempInteger === "bigint") { - tempStr = `${tempInteger}n`; - } else if (typeof tempInteger === "symbol") { - tempStr = "NaN"; - } else { - tempStr = `${parseInt(String(tempInteger), 10)}`; - } - break; - case CHAR_LOWERCASE_F: - // format as a floating point value - const tempFloat = args[++a]; - if (typeof tempFloat === "symbol") { - tempStr = "NaN"; - } else { - tempStr = `${parseFloat(String(tempFloat))}`; - } - break; - case CHAR_LOWERCASE_O: - case CHAR_UPPERCASE_O: - // format as an object - tempStr = stringify(args[++a], new Set(), 0, depth); - break; - case CHAR_PERCENT: - str += first.slice(lastPos, i); - lastPos = i + 1; - continue; - case CHAR_LOWERCASE_C: - // TODO: applies CSS style rules to the output string as specified - continue; - default: - // any other character is not a correct placeholder - continue; - } - - if (lastPos !== i - 1) { - str += first.slice(lastPos, i - 1); - } - - str += tempStr; - lastPos = i + 1; - } else if (nextChar === CHAR_PERCENT) { - str += first.slice(lastPos, i); - lastPos = i + 1; - } - } - } - - if (lastPos !== 0) { - a++; - join = " "; - if (lastPos < first.length) { - str += first.slice(lastPos); - } - } - } - - while (a < args.length) { - const value = args[a]; - str += join; - if (typeof value === "string") { - str += value; - } else { - // use default maximum depth for null or undefined argument - str += stringify(value, new Set(), 0, depth); - } - join = " "; - a++; - } - - if (indentLevel > 0) { - const groupIndent = " ".repeat(indentLevel); - if (str.indexOf("\n") !== -1) { - str = str.replace(/\n/g, `\n${groupIndent}`); - } - str = groupIndent + str; - } - - return str; -} - -type PrintFunc = (x: string, isErr?: boolean) => void; - -const countMap = new Map(); -const timerMap = new Map(); -const isConsoleInstance = Symbol("isConsoleInstance"); - -export class Console { - indentLevel: number; - [isConsoleInstance] = false; - - /** @internal */ - constructor(private printFunc: PrintFunc) { - this.indentLevel = 0; - this[isConsoleInstance] = true; - - // ref https://console.spec.whatwg.org/#console-namespace - // For historical web-compatibility reasons, the namespace object for - // console must have as its [[Prototype]] an empty object, created as if - // by ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%. - const console = Object.create({}) as Console; - Object.assign(console, this); - return console; - } - - /** Writes the arguments to stdout */ - log = (...args: unknown[]): void => { - this.printFunc( - stringifyArgs(args, { - indentLevel: this.indentLevel - }) + "\n", - false - ); - }; - - /** Writes the arguments to stdout */ - debug = this.log; - /** Writes the arguments to stdout */ - info = this.log; - - /** Writes the properties of the supplied `obj` to stdout */ - dir = (obj: unknown, options: ConsoleOptions = {}): void => { - this.printFunc(stringifyArgs([obj], options) + "\n", false); - }; - - /** From MDN: - * Displays an interactive tree of the descendant elements of - * the specified XML/HTML element. If it is not possible to display - * as an element the JavaScript Object view is shown instead. - * The output is presented as a hierarchical listing of expandable - * nodes that let you see the contents of child nodes. - * - * Since we write to stdout, we can't display anything interactive - * we just fall back to `console.dir`. - */ - dirxml = this.dir; - - /** Writes the arguments to stdout */ - warn = (...args: unknown[]): void => { - this.printFunc( - stringifyArgs(args, { - indentLevel: this.indentLevel - }) + "\n", - true - ); - }; - - /** Writes the arguments to stdout */ - error = this.warn; - - /** Writes an error message to stdout if the assertion is `false`. If the - * assertion is `true`, nothing happens. - * - * ref: https://console.spec.whatwg.org/#assert - */ - assert = (condition = false, ...args: unknown[]): void => { - if (condition) { - return; - } - - if (args.length === 0) { - this.error("Assertion failed"); - return; - } - - const [first, ...rest] = args; - - if (typeof first === "string") { - this.error(`Assertion failed: ${first}`, ...rest); - return; - } - - this.error(`Assertion failed:`, ...args); - }; - - count = (label = "default"): void => { - label = String(label); - - if (countMap.has(label)) { - const current = countMap.get(label) || 0; - countMap.set(label, current + 1); - } else { - countMap.set(label, 1); - } - - this.info(`${label}: ${countMap.get(label)}`); - }; - - countReset = (label = "default"): void => { - label = String(label); - - if (countMap.has(label)) { - countMap.set(label, 0); - } else { - this.warn(`Count for '${label}' does not exist`); - } - }; - - table = (data: unknown, properties?: string[]): void => { - if (properties !== undefined && !Array.isArray(properties)) { - throw new Error( - "The 'properties' argument must be of type Array. " + - "Received type string" - ); - } - - if (data === null || typeof data !== "object") { - return this.log(data); - } - - const objectValues: { [key: string]: string[] } = {}; - const indexKeys: string[] = []; - const values: string[] = []; - - const stringifyValue = (value: unknown): string => - stringifyWithQuotes(value, new Set(), 0, 1); - const toTable = (header: string[], body: string[][]): void => - this.log(cliTable(header, body)); - const createColumn = (value: unknown, shift?: number): string[] => [ - ...(shift ? [...new Array(shift)].map((): string => "") : []), - stringifyValue(value) - ]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let resultData: any; - const isSet = data instanceof Set; - const isMap = data instanceof Map; - const valuesKey = "Values"; - const indexKey = isSet || isMap ? "(iteration index)" : "(index)"; - - if (data instanceof Set) { - resultData = [...data]; - } else if (data instanceof Map) { - let idx = 0; - resultData = {}; - - data.forEach((v: unknown, k: unknown): void => { - resultData[idx] = { Key: k, Values: v }; - idx++; - }); - } else { - resultData = data!; - } - - Object.keys(resultData).forEach((k, idx): void => { - const value: unknown = resultData[k]!; - - if (value !== null && typeof value === "object") { - Object.entries(value as { [key: string]: unknown }).forEach( - ([k, v]): void => { - if (properties && !properties.includes(k)) { - return; - } - - if (objectValues[k]) { - objectValues[k].push(stringifyValue(v)); - } else { - objectValues[k] = createColumn(v, idx); - } - } - ); - - values.push(""); - } else { - values.push(stringifyValue(value)); - } - - indexKeys.push(k); - }); - - const headerKeys = Object.keys(objectValues); - const bodyValues = Object.values(objectValues); - const header = [ - indexKey, - ...(properties || [ - ...headerKeys, - !isMap && values.length > 0 && valuesKey - ]) - ].filter(Boolean) as string[]; - const body = [indexKeys, ...bodyValues, values]; - - toTable(header, body); - }; - - time = (label = "default"): void => { - label = String(label); - - if (timerMap.has(label)) { - this.warn(`Timer '${label}' already exists`); - return; - } - - timerMap.set(label, Date.now()); - }; - - timeLog = (label = "default", ...args: unknown[]): void => { - label = String(label); - - if (!timerMap.has(label)) { - this.warn(`Timer '${label}' does not exists`); - return; - } - - const startTime = timerMap.get(label) as number; - const duration = Date.now() - startTime; - - this.info(`${label}: ${duration}ms`, ...args); - }; - - timeEnd = (label = "default"): void => { - label = String(label); - - if (!timerMap.has(label)) { - this.warn(`Timer '${label}' does not exists`); - return; - } - - const startTime = timerMap.get(label) as number; - timerMap.delete(label); - const duration = Date.now() - startTime; - - this.info(`${label}: ${duration}ms`); - }; - - group = (...label: unknown[]): void => { - if (label.length > 0) { - this.log(...label); - } - this.indentLevel += 2; - }; - - groupCollapsed = this.group; - - groupEnd = (): void => { - if (this.indentLevel > 0) { - this.indentLevel -= 2; - } - }; - - clear = (): void => { - this.indentLevel = 0; - cursorTo(stdout, 0, 0); - clearScreenDown(stdout); - }; - - trace = (...args: unknown[]): void => { - const message = stringifyArgs(args, { indentLevel: 0 }); - const err = { - name: "Trace", - message - }; - // @ts-ignore - Error.captureStackTrace(err, this.trace); - this.error((err as Error).stack); - }; - - static [Symbol.hasInstance](instance: Console): boolean { - return instance[isConsoleInstance]; - } -} - -/** A symbol which can be used as a key for a custom method which will be called - * when `Deno.inspect()` is called, or when the object is logged to the console. - */ -export const customInspect = Symbol.for("Deno.customInspect"); - -/** - * `inspect()` converts input into string that has the same format - * as printed by `console.log(...)`; - */ -export function inspect( - value: unknown, - { depth = DEFAULT_MAX_DEPTH }: ConsoleOptions = {} -): string { - if (typeof value === "string") { - return value; - } else { - return stringify(value, new Set(), 0, depth); - } -} - -// Expose these fields to internalObject for tests. -exposeForTest("Console", Console); -exposeForTest("stringifyArgs", stringifyArgs); diff --git a/cli/js/console_table.ts b/cli/js/console_table.ts deleted file mode 100644 index 882f1243b..000000000 --- a/cli/js/console_table.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. MIT license. -// Forked from Node's lib/internal/cli_table.js - -import { TextEncoder } from "./web/text_encoding.ts"; -import { hasOwnProperty } from "./util.ts"; - -const encoder = new TextEncoder(); - -const tableChars = { - middleMiddle: "─", - rowMiddle: "┼", - topRight: "┐", - topLeft: "┌", - leftMiddle: "├", - topMiddle: "┬", - bottomRight: "┘", - bottomLeft: "└", - bottomMiddle: "┴", - rightMiddle: "┤", - left: "│ ", - right: " │", - middle: " │ " -}; - -const colorRegExp = /\u001b\[\d\d?m/g; - -function removeColors(str: string): string { - return str.replace(colorRegExp, ""); -} - -function countBytes(str: string): number { - const normalized = removeColors(String(str)).normalize("NFC"); - - return encoder.encode(normalized).byteLength; -} - -function renderRow(row: string[], columnWidths: number[]): string { - let out = tableChars.left; - for (let i = 0; i < row.length; i++) { - const cell = row[i]; - const len = countBytes(cell); - const needed = (columnWidths[i] - len) / 2; - // round(needed) + ceil(needed) will always add up to the amount - // of spaces we need while also left justifying the output. - out += `${" ".repeat(needed)}${cell}${" ".repeat(Math.ceil(needed))}`; - if (i !== row.length - 1) { - out += tableChars.middle; - } - } - out += tableChars.right; - return out; -} - -export function cliTable(head: string[], columns: string[][]): string { - const rows: string[][] = []; - const columnWidths = head.map((h: string): number => countBytes(h)); - const longestColumn = columns.reduce( - (n: number, a: string[]): number => Math.max(n, a.length), - 0 - ); - - for (let i = 0; i < head.length; i++) { - const column = columns[i]; - for (let j = 0; j < longestColumn; j++) { - if (rows[j] === undefined) { - rows[j] = []; - } - const value = (rows[j][i] = hasOwnProperty(column, j) ? column[j] : ""); - const width = columnWidths[i] || 0; - const counted = countBytes(value); - columnWidths[i] = Math.max(width, counted); - } - } - - const divider = columnWidths.map((i: number): string => - tableChars.middleMiddle.repeat(i + 2) - ); - - let result = - `${tableChars.topLeft}${divider.join(tableChars.topMiddle)}` + - `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` + - `${tableChars.leftMiddle}${divider.join(tableChars.rowMiddle)}` + - `${tableChars.rightMiddle}\n`; - - for (const row of rows) { - result += `${renderRow(row, columnWidths)}\n`; - } - - result += - `${tableChars.bottomLeft}${divider.join(tableChars.bottomMiddle)}` + - tableChars.bottomRight; - - return result; -} diff --git a/cli/js/deno.ts b/cli/js/deno.ts index f0115f261..4f56bb7d1 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -11,8 +11,8 @@ export { export { build, OperatingSystem, Arch } from "./build.ts"; export { chmodSync, chmod } from "./ops/fs/chmod.ts"; export { chownSync, chown } from "./ops/fs/chown.ts"; -export { transpileOnly, compile, bundle } from "./compiler_api.ts"; -export { inspect } from "./console.ts"; +export { transpileOnly, compile, bundle } from "./compiler/api.ts"; +export { inspect } from "./web/console.ts"; export { copyFileSync, copyFile } from "./ops/fs/copy_file.ts"; export { Diagnostic, diff --git a/cli/js/globals.ts b/cli/js/globals.ts index 7033afd92..8d122878f 100644 --- a/cli/js/globals.ts +++ b/cli/js/globals.ts @@ -1,7 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import * as blob from "./web/blob.ts"; -import * as consoleTypes from "./console.ts"; +import * as consoleTypes from "./web/console.ts"; import * as customEvent from "./web/custom_event.ts"; import * as domTypes from "./web/dom_types.ts"; import * as domFile from "./web/dom_file.ts"; @@ -11,11 +11,11 @@ import * as formData from "./web/form_data.ts"; import * as fetchTypes from "./web/fetch.ts"; import * as headers from "./web/headers.ts"; import * as textEncoding from "./web/text_encoding.ts"; -import * as timers from "./timers.ts"; +import * as timers from "./web/timers.ts"; import * as url from "./web/url.ts"; import * as urlSearchParams from "./web/url_search_params.ts"; -import * as workers from "./workers.ts"; -import * as performanceUtil from "./performance.ts"; +import * as workers from "./web/workers.ts"; +import * as performanceUtil from "./web/performance.ts"; import * as request from "./web/request.ts"; // These imports are not exposed and therefore are fine to just import the diff --git a/cli/js/mock_builtin.js b/cli/js/mock_builtin.js deleted file mode 100644 index 0f5bd2a44..000000000 --- a/cli/js/mock_builtin.js +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -export default undefined; diff --git a/cli/js/performance.ts b/cli/js/performance.ts deleted file mode 100644 index 7aaa35952..000000000 --- a/cli/js/performance.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { now as opNow } from "./ops/timers.ts"; - -export class Performance { - /** Returns a current time from Deno's start in milliseconds. - * - * Use the flag --allow-hrtime return a precise value. - * - * const t = performance.now(); - * console.log(`${t} ms since start!`); - */ - now(): number { - const res = opNow(); - return res.seconds * 1e3 + res.subsecNanos / 1e6; - } -} diff --git a/cli/js/repl.ts b/cli/js/repl.ts index 8325159dd..581834cfd 100644 --- a/cli/js/repl.ts +++ b/cli/js/repl.ts @@ -1,7 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { exit } from "./ops/os.ts"; import { core } from "./core.ts"; -import { stringifyArgs } from "./console.ts"; +import { stringifyArgs } from "./web/console.ts"; import { startRepl, readline } from "./ops/repl.ts"; import { close } from "./ops/resources.ts"; diff --git a/cli/js/symbols.ts b/cli/js/symbols.ts index 4a8a6abfe..9d8928cf2 100644 --- a/cli/js/symbols.ts +++ b/cli/js/symbols.ts @@ -1,6 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { internalSymbol } from "./internals.ts"; -import { customInspect } from "./console.ts"; +import { customInspect } from "./web/console.ts"; /** Special Deno related symbols. */ export const symbols = { diff --git a/cli/js/testing.ts b/cli/js/testing.ts index f851b8fcc..f1318f0ce 100644 --- a/cli/js/testing.ts +++ b/cli/js/testing.ts @@ -1,7 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { red, green, bgRed, gray, italic } from "./colors.ts"; import { exit } from "./ops/os.ts"; -import { Console } from "./console.ts"; +import { Console } from "./web/console.ts"; function formatDuration(time = 0): string { const timeStr = `(${time}ms)`; diff --git a/cli/js/timers.ts b/cli/js/timers.ts deleted file mode 100644 index 844a0b204..000000000 --- a/cli/js/timers.ts +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { assert } from "./util.ts"; -import { startGlobalTimer, stopGlobalTimer } from "./ops/timers.ts"; -import { RBTree } from "./rbtree.ts"; - -const { console } = globalThis; - -interface Timer { - id: number; - callback: () => void; - delay: number; - due: number; - repeat: boolean; - scheduled: boolean; -} - -// Timeout values > TIMEOUT_MAX are set to 1. -const TIMEOUT_MAX = 2 ** 31 - 1; - -let globalTimeoutDue: number | null = null; - -let nextTimerId = 1; -const idMap = new Map(); -type DueNode = { due: number; timers: Timer[] }; -const dueTree = new RBTree((a, b) => a.due - b.due); - -function clearGlobalTimeout(): void { - globalTimeoutDue = null; - stopGlobalTimer(); -} - -let pendingEvents = 0; -const pendingFireTimers: Timer[] = []; -let hasPendingFireTimers = false; -let pendingScheduleTimers: Timer[] = []; - -async function setGlobalTimeout(due: number, now: number): Promise { - // Since JS and Rust don't use the same clock, pass the time to rust as a - // relative time value. On the Rust side we'll turn that into an absolute - // value again. - const timeout = due - now; - assert(timeout >= 0); - // Send message to the backend. - globalTimeoutDue = due; - pendingEvents++; - // FIXME(bartlomieju): this is problematic, because `clearGlobalTimeout` - // is synchronous. That means that timer is cancelled, but this promise is still pending - // until next turn of event loop. This leads to "leaking of async ops" in tests; - // because `clearTimeout/clearInterval` might be the last statement in test function - // `opSanitizer` will immediately complain that there is pending op going on, unless - // some timeout/defer is put in place to allow promise resolution. - // Ideally `clearGlobalTimeout` doesn't return until this op is resolved, but - // I'm not if that's possible. - await startGlobalTimer(timeout); - pendingEvents--; - // eslint-disable-next-line @typescript-eslint/no-use-before-define - fireTimers(); -} - -function setOrClearGlobalTimeout(due: number | null, now: number): void { - if (due == null) { - clearGlobalTimeout(); - } else { - setGlobalTimeout(due, now); - } -} - -function schedule(timer: Timer, now: number): void { - assert(!timer.scheduled); - assert(now <= timer.due); - // There are more timers pending firing. - // We must ensure new timer scheduled after them. - // Push them to a queue that would be depleted after last pending fire - // timer is fired. - // (This also implies behavior of setInterval) - if (hasPendingFireTimers) { - pendingScheduleTimers.push(timer); - return; - } - // Find or create the list of timers that will fire at point-in-time `due`. - const maybeNewDueNode = { due: timer.due, timers: [] }; - let dueNode = dueTree.find(maybeNewDueNode); - if (dueNode === null) { - dueTree.insert(maybeNewDueNode); - dueNode = maybeNewDueNode; - } - // Append the newly scheduled timer to the list and mark it as scheduled. - dueNode!.timers.push(timer); - timer.scheduled = true; - // If the new timer is scheduled to fire before any timer that existed before, - // update the global timeout to reflect this. - if (globalTimeoutDue === null || globalTimeoutDue > timer.due) { - setOrClearGlobalTimeout(timer.due, now); - } -} - -function unschedule(timer: Timer): void { - // Check if our timer is pending scheduling or pending firing. - // If either is true, they are not in tree, and their idMap entry - // will be deleted soon. Remove it from queue. - let index = -1; - if ((index = pendingScheduleTimers.indexOf(timer)) >= 0) { - pendingScheduleTimers.splice(index); - return; - } - if ((index = pendingFireTimers.indexOf(timer)) >= 0) { - pendingFireTimers.splice(index); - return; - } - // If timer is not in the 2 pending queues and is unscheduled, - // it is not in the tree. - if (!timer.scheduled) { - return; - } - const searchKey = { due: timer.due, timers: [] }; - // Find the list of timers that will fire at point-in-time `due`. - const list = dueTree.find(searchKey)!.timers; - if (list.length === 1) { - // Time timer is the only one in the list. Remove the entire list. - assert(list[0] === timer); - dueTree.remove(searchKey); - // If the unscheduled timer was 'next up', find when the next timer that - // still exists is due, and update the global alarm accordingly. - if (timer.due === globalTimeoutDue) { - const nextDueNode: DueNode | null = dueTree.min(); - setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, Date.now()); - } - } else { - // Multiple timers that are due at the same point in time. - // Remove this timer from the list. - const index = list.indexOf(timer); - assert(index > -1); - list.splice(index, 1); - } -} - -function fire(timer: Timer): void { - // If the timer isn't found in the ID map, that means it has been cancelled - // between the timer firing and the promise callback (this function). - if (!idMap.has(timer.id)) { - return; - } - // Reschedule the timer if it is a repeating one, otherwise drop it. - if (!timer.repeat) { - // One-shot timer: remove the timer from this id-to-timer map. - idMap.delete(timer.id); - } else { - // Interval timer: compute when timer was supposed to fire next. - // However make sure to never schedule the next interval in the past. - const now = Date.now(); - timer.due = Math.max(now, timer.due + timer.delay); - schedule(timer, now); - } - // Call the user callback. Intermediate assignment is to avoid leaking `this` - // to it, while also keeping the stack trace neat when it shows up in there. - const callback = timer.callback; - callback(); -} - -function fireTimers(): void { - const now = Date.now(); - // Bail out if we're not expecting the global timer to fire. - if (globalTimeoutDue === null || pendingEvents > 0) { - return; - } - // After firing the timers that are due now, this will hold the first timer - // list that hasn't fired yet. - let nextDueNode: DueNode | null; - while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { - dueTree.remove(nextDueNode); - // Fire all the timers in the list. - for (const timer of nextDueNode.timers) { - // With the list dropped, the timer is no longer scheduled. - timer.scheduled = false; - // Place the callback to pending timers to fire. - pendingFireTimers.push(timer); - } - } - if (pendingFireTimers.length > 0) { - hasPendingFireTimers = true; - // Fire the list of pending timers as a chain of microtasks. - globalThis.queueMicrotask(firePendingTimers); - } else { - setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); - } -} - -function firePendingTimers(): void { - if (pendingFireTimers.length === 0) { - // All timer tasks are done. - hasPendingFireTimers = false; - // Schedule all new timers pushed during previous timer executions - const now = Date.now(); - for (const newTimer of pendingScheduleTimers) { - newTimer.due = Math.max(newTimer.due, now); - schedule(newTimer, now); - } - pendingScheduleTimers = []; - // Reschedule for next round of timeout. - const nextDueNode = dueTree.min(); - const due = nextDueNode && Math.max(nextDueNode.due, now); - setOrClearGlobalTimeout(due, now); - } else { - // Fire a single timer and allow its children microtasks scheduled first. - fire(pendingFireTimers.shift()!); - // ...and we schedule next timer after this. - globalThis.queueMicrotask(firePendingTimers); - } -} - -export type Args = unknown[]; - -function checkThis(thisArg: unknown): void { - if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { - throw new TypeError("Illegal invocation"); - } -} - -function checkBigInt(n: unknown): void { - if (typeof n === "bigint") { - throw new TypeError("Cannot convert a BigInt value to a number"); - } -} - -function setTimer( - cb: (...args: Args) => void, - delay: number, - args: Args, - repeat: boolean -): number { - // Bind `args` to the callback and bind `this` to globalThis(global). - const callback: () => void = cb.bind(globalThis, ...args); - // In the browser, the delay value must be coercible to an integer between 0 - // and INT32_MAX. Any other value will cause the timer to fire immediately. - // We emulate this behavior. - const now = Date.now(); - if (delay > TIMEOUT_MAX) { - console.warn( - `${delay} does not fit into` + - " a 32-bit signed integer." + - "\nTimeout duration was set to 1." - ); - delay = 1; - } - delay = Math.max(0, delay | 0); - - // Create a new, unscheduled timer object. - const timer = { - id: nextTimerId++, - callback, - args, - delay, - due: now + delay, - repeat, - scheduled: false - }; - // Register the timer's existence in the id-to-timer map. - idMap.set(timer.id, timer); - // Schedule the timer in the due table. - schedule(timer, now); - return timer.id; -} - -/** Sets a timer which executes a function once after the timer expires. */ -export function setTimeout( - cb: (...args: Args) => void, - delay = 0, - ...args: Args -): number { - checkBigInt(delay); - // @ts-ignore - checkThis(this); - return setTimer(cb, delay, args, false); -} - -/** Repeatedly calls a function, with a fixed time delay between each call. */ -export function setInterval( - cb: (...args: Args) => void, - delay = 0, - ...args: Args -): number { - checkBigInt(delay); - // @ts-ignore - checkThis(this); - return setTimer(cb, delay, args, true); -} - -/** Clears a previously set timer by id. AKA clearTimeout and clearInterval. */ -function clearTimer(id: number): void { - id = Number(id); - const timer = idMap.get(id); - if (timer === undefined) { - // Timer doesn't exist any more or never existed. This is not an error. - return; - } - // Unschedule the timer if it is currently scheduled, and forget about it. - unschedule(timer); - idMap.delete(timer.id); -} - -export function clearTimeout(id = 0): void { - checkBigInt(id); - if (id === 0) { - return; - } - clearTimer(id); -} - -export function clearInterval(id = 0): void { - checkBigInt(id); - if (id === 0) { - return; - } - clearTimer(id); -} diff --git a/cli/js/ts_global.d.ts b/cli/js/ts_global.d.ts deleted file mode 100644 index 7b9d84c7a..000000000 --- a/cli/js/ts_global.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// 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; - } -} diff --git a/cli/js/web/README.md b/cli/js/web/README.md new file mode 100644 index 000000000..865f4e0fb --- /dev/null +++ b/cli/js/web/README.md @@ -0,0 +1,8 @@ +# Deno Web APIs + +This directory facilities Web APIs that are available in Deno. + +Please note, that some of implementations might not be completely aligned with +specification. + +Some of the Web APIs are using ops under the hood, eg. `console`, `performance`. diff --git a/cli/js/web/console.ts b/cli/js/web/console.ts new file mode 100644 index 000000000..601d5fd09 --- /dev/null +++ b/cli/js/web/console.ts @@ -0,0 +1,776 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { isTypedArray } from "../util.ts"; +import { TypedArray } from "../types.ts"; +import { TextEncoder } from "./text_encoding.ts"; +import { File, stdout } from "../files.ts"; +import { cliTable } from "./console_table.ts"; +import { exposeForTest } from "../internals.ts"; + +type ConsoleContext = Set; +type ConsoleOptions = Partial<{ + showHidden: boolean; + depth: number; + colors: boolean; + indentLevel: number; +}>; + +// Default depth of logging nested objects +const DEFAULT_MAX_DEPTH = 4; + +// Number of elements an object must have before it's displayed in appreviated +// form. +const OBJ_ABBREVIATE_SIZE = 5; + +const STR_ABBREVIATE_SIZE = 100; + +// Char codes +const CHAR_PERCENT = 37; /* % */ +const CHAR_LOWERCASE_S = 115; /* s */ +const CHAR_LOWERCASE_D = 100; /* d */ +const CHAR_LOWERCASE_I = 105; /* i */ +const CHAR_LOWERCASE_F = 102; /* f */ +const CHAR_LOWERCASE_O = 111; /* o */ +const CHAR_UPPERCASE_O = 79; /* O */ +const CHAR_LOWERCASE_C = 99; /* c */ +export class CSI { + static kClear = "\x1b[1;1H"; + static kClearScreenDown = "\x1b[0J"; +} + +/* eslint-disable @typescript-eslint/no-use-before-define */ + +function cursorTo(stream: File, _x: number, _y?: number): void { + const uint8 = new TextEncoder().encode(CSI.kClear); + stream.writeSync(uint8); +} + +function clearScreenDown(stream: File): void { + const uint8 = new TextEncoder().encode(CSI.kClearScreenDown); + stream.writeSync(uint8); +} + +function getClassInstanceName(instance: unknown): string { + if (typeof instance !== "object") { + return ""; + } + if (!instance) { + return ""; + } + + const proto = Object.getPrototypeOf(instance); + if (proto && proto.constructor) { + return proto.constructor.name; // could be "Object" or "Array" + } + + return ""; +} + +function createFunctionString(value: Function, _ctx: ConsoleContext): string { + // Might be Function/AsyncFunction/GeneratorFunction + const cstrName = Object.getPrototypeOf(value).constructor.name; + if (value.name && value.name !== "anonymous") { + // from MDN spec + return `[${cstrName}: ${value.name}]`; + } + return `[${cstrName}]`; +} + +interface IterablePrintConfig { + typeName: string; + displayName: string; + delims: [string, string]; + entryHandler: ( + entry: T, + ctx: ConsoleContext, + level: number, + maxLevel: number + ) => string; +} + +function createIterableString( + value: Iterable, + ctx: ConsoleContext, + level: number, + maxLevel: number, + config: IterablePrintConfig +): string { + if (level >= maxLevel) { + return `[${config.typeName}]`; + } + ctx.add(value); + + const entries: string[] = []; + // In cases e.g. Uint8Array.prototype + try { + for (const el of value) { + entries.push(config.entryHandler(el, ctx, level + 1, maxLevel)); + } + } catch (e) {} + ctx.delete(value); + const iPrefix = `${config.displayName ? config.displayName + " " : ""}`; + const iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; + return `${iPrefix}${config.delims[0]}${iContent}${config.delims[1]}`; +} + +function stringify( + value: unknown, + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + switch (typeof value) { + case "string": + return value; + case "number": + // Special handling of -0 + return Object.is(value, -0) ? "-0" : `${value}`; + case "boolean": + case "undefined": + case "symbol": + return String(value); + case "bigint": + return `${value}n`; + case "function": + return createFunctionString(value as Function, ctx); + case "object": + if (value === null) { + return "null"; + } + + if (ctx.has(value)) { + return "[Circular]"; + } + + return createObjectString(value, ctx, level, maxLevel); + default: + return "[Not Implemented]"; + } +} + +// Print strings when they are inside of arrays or objects with quotes +function stringifyWithQuotes( + value: unknown, + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + switch (typeof value) { + case "string": + const trunc = + value.length > STR_ABBREVIATE_SIZE + ? value.slice(0, STR_ABBREVIATE_SIZE) + "..." + : value; + return JSON.stringify(trunc); + default: + return stringify(value, ctx, level, maxLevel); + } +} + +function createArrayString( + value: unknown[], + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + const printConfig: IterablePrintConfig = { + typeName: "Array", + displayName: "", + delims: ["[", "]"], + entryHandler: (el, ctx, level, maxLevel): string => + stringifyWithQuotes(el, ctx, level + 1, maxLevel) + }; + return createIterableString(value, ctx, level, maxLevel, printConfig); +} + +function createTypedArrayString( + typedArrayName: string, + value: TypedArray, + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + const printConfig: IterablePrintConfig = { + typeName: typedArrayName, + displayName: typedArrayName, + delims: ["[", "]"], + entryHandler: (el, ctx, level, maxLevel): string => + stringifyWithQuotes(el, ctx, level + 1, maxLevel) + }; + return createIterableString(value, ctx, level, maxLevel, printConfig); +} + +function createSetString( + value: Set, + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + const printConfig: IterablePrintConfig = { + typeName: "Set", + displayName: "Set", + delims: ["{", "}"], + entryHandler: (el, ctx, level, maxLevel): string => + stringifyWithQuotes(el, ctx, level + 1, maxLevel) + }; + return createIterableString(value, ctx, level, maxLevel, printConfig); +} + +function createMapString( + value: Map, + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + const printConfig: IterablePrintConfig<[unknown, unknown]> = { + typeName: "Map", + displayName: "Map", + delims: ["{", "}"], + entryHandler: (el, ctx, level, maxLevel): string => { + const [key, val] = el; + return `${stringifyWithQuotes( + key, + ctx, + level + 1, + maxLevel + )} => ${stringifyWithQuotes(val, ctx, level + 1, maxLevel)}`; + } + }; + return createIterableString(value, ctx, level, maxLevel, printConfig); +} + +function createWeakSetString(): string { + return "WeakSet { [items unknown] }"; // as seen in Node +} + +function createWeakMapString(): string { + return "WeakMap { [items unknown] }"; // as seen in Node +} + +function createDateString(value: Date): string { + // without quotes, ISO format + return value.toISOString(); +} + +function createRegExpString(value: RegExp): string { + return value.toString(); +} + +/* eslint-disable @typescript-eslint/ban-types */ + +function createStringWrapperString(value: String): string { + return `[String: "${value.toString()}"]`; +} + +function createBooleanWrapperString(value: Boolean): string { + return `[Boolean: ${value.toString()}]`; +} + +function createNumberWrapperString(value: Number): string { + return `[Number: ${value.toString()}]`; +} + +/* eslint-enable @typescript-eslint/ban-types */ + +// TODO: Promise, requires v8 bindings to get info +// TODO: Proxy + +function createRawObjectString( + value: { [key: string]: unknown }, + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + if (level >= maxLevel) { + return "[Object]"; + } + ctx.add(value); + + let baseString = ""; + + const className = getClassInstanceName(value); + let shouldShowClassName = false; + if (className && className !== "Object" && className !== "anonymous") { + shouldShowClassName = true; + } + const keys = Object.keys(value); + const entries: string[] = keys.map((key): string => { + if (keys.length > OBJ_ABBREVIATE_SIZE) { + return key; + } else { + return `${key}: ${stringifyWithQuotes( + value[key], + ctx, + level + 1, + maxLevel + )}`; + } + }); + + ctx.delete(value); + + if (entries.length === 0) { + baseString = "{}"; + } else { + baseString = `{ ${entries.join(", ")} }`; + } + + if (shouldShowClassName) { + baseString = `${className} ${baseString}`; + } + + return baseString; +} + +function createObjectString( + value: {}, + ...args: [ConsoleContext, number, number] +): string { + if (customInspect in value && typeof value[customInspect] === "function") { + try { + return String(value[customInspect]!()); + } catch {} + } + if (value instanceof Error) { + return String(value.stack); + } else if (Array.isArray(value)) { + return createArrayString(value, ...args); + } else if (value instanceof Number) { + return createNumberWrapperString(value); + } else if (value instanceof Boolean) { + return createBooleanWrapperString(value); + } else if (value instanceof String) { + return createStringWrapperString(value); + } else if (value instanceof RegExp) { + return createRegExpString(value); + } else if (value instanceof Date) { + return createDateString(value); + } else if (value instanceof Set) { + return createSetString(value, ...args); + } else if (value instanceof Map) { + return createMapString(value, ...args); + } else if (value instanceof WeakSet) { + return createWeakSetString(); + } else if (value instanceof WeakMap) { + return createWeakMapString(); + } else if (isTypedArray(value)) { + return createTypedArrayString( + Object.getPrototypeOf(value).constructor.name, + value, + ...args + ); + } else { + // Otherwise, default object formatting + return createRawObjectString(value, ...args); + } +} + +/** @internal */ +export function stringifyArgs( + args: unknown[], + { depth = DEFAULT_MAX_DEPTH, indentLevel = 0 }: ConsoleOptions = {} +): string { + const first = args[0]; + let a = 0; + let str = ""; + let join = ""; + + if (typeof first === "string") { + let tempStr: string; + let lastPos = 0; + + for (let i = 0; i < first.length - 1; i++) { + if (first.charCodeAt(i) === CHAR_PERCENT) { + const nextChar = first.charCodeAt(++i); + if (a + 1 !== args.length) { + switch (nextChar) { + case CHAR_LOWERCASE_S: + // format as a string + tempStr = String(args[++a]); + break; + case CHAR_LOWERCASE_D: + case CHAR_LOWERCASE_I: + // format as an integer + const tempInteger = args[++a]; + if (typeof tempInteger === "bigint") { + tempStr = `${tempInteger}n`; + } else if (typeof tempInteger === "symbol") { + tempStr = "NaN"; + } else { + tempStr = `${parseInt(String(tempInteger), 10)}`; + } + break; + case CHAR_LOWERCASE_F: + // format as a floating point value + const tempFloat = args[++a]; + if (typeof tempFloat === "symbol") { + tempStr = "NaN"; + } else { + tempStr = `${parseFloat(String(tempFloat))}`; + } + break; + case CHAR_LOWERCASE_O: + case CHAR_UPPERCASE_O: + // format as an object + tempStr = stringify(args[++a], new Set(), 0, depth); + break; + case CHAR_PERCENT: + str += first.slice(lastPos, i); + lastPos = i + 1; + continue; + case CHAR_LOWERCASE_C: + // TODO: applies CSS style rules to the output string as specified + continue; + default: + // any other character is not a correct placeholder + continue; + } + + if (lastPos !== i - 1) { + str += first.slice(lastPos, i - 1); + } + + str += tempStr; + lastPos = i + 1; + } else if (nextChar === CHAR_PERCENT) { + str += first.slice(lastPos, i); + lastPos = i + 1; + } + } + } + + if (lastPos !== 0) { + a++; + join = " "; + if (lastPos < first.length) { + str += first.slice(lastPos); + } + } + } + + while (a < args.length) { + const value = args[a]; + str += join; + if (typeof value === "string") { + str += value; + } else { + // use default maximum depth for null or undefined argument + str += stringify(value, new Set(), 0, depth); + } + join = " "; + a++; + } + + if (indentLevel > 0) { + const groupIndent = " ".repeat(indentLevel); + if (str.indexOf("\n") !== -1) { + str = str.replace(/\n/g, `\n${groupIndent}`); + } + str = groupIndent + str; + } + + return str; +} + +type PrintFunc = (x: string, isErr?: boolean) => void; + +const countMap = new Map(); +const timerMap = new Map(); +const isConsoleInstance = Symbol("isConsoleInstance"); + +export class Console { + indentLevel: number; + [isConsoleInstance] = false; + + /** @internal */ + constructor(private printFunc: PrintFunc) { + this.indentLevel = 0; + this[isConsoleInstance] = true; + + // ref https://console.spec.whatwg.org/#console-namespace + // For historical web-compatibility reasons, the namespace object for + // console must have as its [[Prototype]] an empty object, created as if + // by ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%. + const console = Object.create({}) as Console; + Object.assign(console, this); + return console; + } + + /** Writes the arguments to stdout */ + log = (...args: unknown[]): void => { + this.printFunc( + stringifyArgs(args, { + indentLevel: this.indentLevel + }) + "\n", + false + ); + }; + + /** Writes the arguments to stdout */ + debug = this.log; + /** Writes the arguments to stdout */ + info = this.log; + + /** Writes the properties of the supplied `obj` to stdout */ + dir = (obj: unknown, options: ConsoleOptions = {}): void => { + this.printFunc(stringifyArgs([obj], options) + "\n", false); + }; + + /** From MDN: + * Displays an interactive tree of the descendant elements of + * the specified XML/HTML element. If it is not possible to display + * as an element the JavaScript Object view is shown instead. + * The output is presented as a hierarchical listing of expandable + * nodes that let you see the contents of child nodes. + * + * Since we write to stdout, we can't display anything interactive + * we just fall back to `console.dir`. + */ + dirxml = this.dir; + + /** Writes the arguments to stdout */ + warn = (...args: unknown[]): void => { + this.printFunc( + stringifyArgs(args, { + indentLevel: this.indentLevel + }) + "\n", + true + ); + }; + + /** Writes the arguments to stdout */ + error = this.warn; + + /** Writes an error message to stdout if the assertion is `false`. If the + * assertion is `true`, nothing happens. + * + * ref: https://console.spec.whatwg.org/#assert + */ + assert = (condition = false, ...args: unknown[]): void => { + if (condition) { + return; + } + + if (args.length === 0) { + this.error("Assertion failed"); + return; + } + + const [first, ...rest] = args; + + if (typeof first === "string") { + this.error(`Assertion failed: ${first}`, ...rest); + return; + } + + this.error(`Assertion failed:`, ...args); + }; + + count = (label = "default"): void => { + label = String(label); + + if (countMap.has(label)) { + const current = countMap.get(label) || 0; + countMap.set(label, current + 1); + } else { + countMap.set(label, 1); + } + + this.info(`${label}: ${countMap.get(label)}`); + }; + + countReset = (label = "default"): void => { + label = String(label); + + if (countMap.has(label)) { + countMap.set(label, 0); + } else { + this.warn(`Count for '${label}' does not exist`); + } + }; + + table = (data: unknown, properties?: string[]): void => { + if (properties !== undefined && !Array.isArray(properties)) { + throw new Error( + "The 'properties' argument must be of type Array. " + + "Received type string" + ); + } + + if (data === null || typeof data !== "object") { + return this.log(data); + } + + const objectValues: { [key: string]: string[] } = {}; + const indexKeys: string[] = []; + const values: string[] = []; + + const stringifyValue = (value: unknown): string => + stringifyWithQuotes(value, new Set(), 0, 1); + const toTable = (header: string[], body: string[][]): void => + this.log(cliTable(header, body)); + const createColumn = (value: unknown, shift?: number): string[] => [ + ...(shift ? [...new Array(shift)].map((): string => "") : []), + stringifyValue(value) + ]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let resultData: any; + const isSet = data instanceof Set; + const isMap = data instanceof Map; + const valuesKey = "Values"; + const indexKey = isSet || isMap ? "(iteration index)" : "(index)"; + + if (data instanceof Set) { + resultData = [...data]; + } else if (data instanceof Map) { + let idx = 0; + resultData = {}; + + data.forEach((v: unknown, k: unknown): void => { + resultData[idx] = { Key: k, Values: v }; + idx++; + }); + } else { + resultData = data!; + } + + Object.keys(resultData).forEach((k, idx): void => { + const value: unknown = resultData[k]!; + + if (value !== null && typeof value === "object") { + Object.entries(value as { [key: string]: unknown }).forEach( + ([k, v]): void => { + if (properties && !properties.includes(k)) { + return; + } + + if (objectValues[k]) { + objectValues[k].push(stringifyValue(v)); + } else { + objectValues[k] = createColumn(v, idx); + } + } + ); + + values.push(""); + } else { + values.push(stringifyValue(value)); + } + + indexKeys.push(k); + }); + + const headerKeys = Object.keys(objectValues); + const bodyValues = Object.values(objectValues); + const header = [ + indexKey, + ...(properties || [ + ...headerKeys, + !isMap && values.length > 0 && valuesKey + ]) + ].filter(Boolean) as string[]; + const body = [indexKeys, ...bodyValues, values]; + + toTable(header, body); + }; + + time = (label = "default"): void => { + label = String(label); + + if (timerMap.has(label)) { + this.warn(`Timer '${label}' already exists`); + return; + } + + timerMap.set(label, Date.now()); + }; + + timeLog = (label = "default", ...args: unknown[]): void => { + label = String(label); + + if (!timerMap.has(label)) { + this.warn(`Timer '${label}' does not exists`); + return; + } + + const startTime = timerMap.get(label) as number; + const duration = Date.now() - startTime; + + this.info(`${label}: ${duration}ms`, ...args); + }; + + timeEnd = (label = "default"): void => { + label = String(label); + + if (!timerMap.has(label)) { + this.warn(`Timer '${label}' does not exists`); + return; + } + + const startTime = timerMap.get(label) as number; + timerMap.delete(label); + const duration = Date.now() - startTime; + + this.info(`${label}: ${duration}ms`); + }; + + group = (...label: unknown[]): void => { + if (label.length > 0) { + this.log(...label); + } + this.indentLevel += 2; + }; + + groupCollapsed = this.group; + + groupEnd = (): void => { + if (this.indentLevel > 0) { + this.indentLevel -= 2; + } + }; + + clear = (): void => { + this.indentLevel = 0; + cursorTo(stdout, 0, 0); + clearScreenDown(stdout); + }; + + trace = (...args: unknown[]): void => { + const message = stringifyArgs(args, { indentLevel: 0 }); + const err = { + name: "Trace", + message + }; + // @ts-ignore + Error.captureStackTrace(err, this.trace); + this.error((err as Error).stack); + }; + + static [Symbol.hasInstance](instance: Console): boolean { + return instance[isConsoleInstance]; + } +} + +/** A symbol which can be used as a key for a custom method which will be called + * when `Deno.inspect()` is called, or when the object is logged to the console. + */ +export const customInspect = Symbol.for("Deno.customInspect"); + +/** + * `inspect()` converts input into string that has the same format + * as printed by `console.log(...)`; + */ +export function inspect( + value: unknown, + { depth = DEFAULT_MAX_DEPTH }: ConsoleOptions = {} +): string { + if (typeof value === "string") { + return value; + } else { + return stringify(value, new Set(), 0, depth); + } +} + +// Expose these fields to internalObject for tests. +exposeForTest("Console", Console); +exposeForTest("stringifyArgs", stringifyArgs); diff --git a/cli/js/web/console_table.ts b/cli/js/web/console_table.ts new file mode 100644 index 000000000..276d77f1d --- /dev/null +++ b/cli/js/web/console_table.ts @@ -0,0 +1,94 @@ +// Copyright Joyent, Inc. and other Node contributors. MIT license. +// Forked from Node's lib/internal/cli_table.js + +import { TextEncoder } from "./text_encoding.ts"; +import { hasOwnProperty } from "../util.ts"; + +const encoder = new TextEncoder(); + +const tableChars = { + middleMiddle: "─", + rowMiddle: "┼", + topRight: "┐", + topLeft: "┌", + leftMiddle: "├", + topMiddle: "┬", + bottomRight: "┘", + bottomLeft: "└", + bottomMiddle: "┴", + rightMiddle: "┤", + left: "│ ", + right: " │", + middle: " │ " +}; + +const colorRegExp = /\u001b\[\d\d?m/g; + +function removeColors(str: string): string { + return str.replace(colorRegExp, ""); +} + +function countBytes(str: string): number { + const normalized = removeColors(String(str)).normalize("NFC"); + + return encoder.encode(normalized).byteLength; +} + +function renderRow(row: string[], columnWidths: number[]): string { + let out = tableChars.left; + for (let i = 0; i < row.length; i++) { + const cell = row[i]; + const len = countBytes(cell); + const needed = (columnWidths[i] - len) / 2; + // round(needed) + ceil(needed) will always add up to the amount + // of spaces we need while also left justifying the output. + out += `${" ".repeat(needed)}${cell}${" ".repeat(Math.ceil(needed))}`; + if (i !== row.length - 1) { + out += tableChars.middle; + } + } + out += tableChars.right; + return out; +} + +export function cliTable(head: string[], columns: string[][]): string { + const rows: string[][] = []; + const columnWidths = head.map((h: string): number => countBytes(h)); + const longestColumn = columns.reduce( + (n: number, a: string[]): number => Math.max(n, a.length), + 0 + ); + + for (let i = 0; i < head.length; i++) { + const column = columns[i]; + for (let j = 0; j < longestColumn; j++) { + if (rows[j] === undefined) { + rows[j] = []; + } + const value = (rows[j][i] = hasOwnProperty(column, j) ? column[j] : ""); + const width = columnWidths[i] || 0; + const counted = countBytes(value); + columnWidths[i] = Math.max(width, counted); + } + } + + const divider = columnWidths.map((i: number): string => + tableChars.middleMiddle.repeat(i + 2) + ); + + let result = + `${tableChars.topLeft}${divider.join(tableChars.topMiddle)}` + + `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` + + `${tableChars.leftMiddle}${divider.join(tableChars.rowMiddle)}` + + `${tableChars.rightMiddle}\n`; + + for (const row of rows) { + result += `${renderRow(row, columnWidths)}\n`; + } + + result += + `${tableChars.bottomLeft}${divider.join(tableChars.bottomMiddle)}` + + tableChars.bottomRight; + + return result; +} diff --git a/cli/js/web/headers.ts b/cli/js/web/headers.ts index 65d52cacd..652dd2de6 100644 --- a/cli/js/web/headers.ts +++ b/cli/js/web/headers.ts @@ -2,7 +2,7 @@ import * as domTypes from "./dom_types.ts"; import { DomIterableMixin } from "./dom_iterable.ts"; import { requiredArguments } from "../util.ts"; -import { customInspect } from "../console.ts"; +import { customInspect } from "./console.ts"; // From node-fetch // Copyright (c) 2016 David Frank. MIT License. diff --git a/cli/js/web/performance.ts b/cli/js/web/performance.ts new file mode 100644 index 000000000..cb4daa846 --- /dev/null +++ b/cli/js/web/performance.ts @@ -0,0 +1,16 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { now as opNow } from "../ops/timers.ts"; + +export class Performance { + /** Returns a current time from Deno's start in milliseconds. + * + * Use the flag --allow-hrtime return a precise value. + * + * const t = performance.now(); + * console.log(`${t} ms since start!`); + */ + now(): number { + const res = opNow(); + return res.seconds * 1e3 + res.subsecNanos / 1e6; + } +} diff --git a/cli/js/web/timers.ts b/cli/js/web/timers.ts new file mode 100644 index 000000000..806b7c160 --- /dev/null +++ b/cli/js/web/timers.ts @@ -0,0 +1,315 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { assert } from "../util.ts"; +import { startGlobalTimer, stopGlobalTimer } from "../ops/timers.ts"; +import { RBTree } from "../rbtree.ts"; + +const { console } = globalThis; + +interface Timer { + id: number; + callback: () => void; + delay: number; + due: number; + repeat: boolean; + scheduled: boolean; +} + +// Timeout values > TIMEOUT_MAX are set to 1. +const TIMEOUT_MAX = 2 ** 31 - 1; + +let globalTimeoutDue: number | null = null; + +let nextTimerId = 1; +const idMap = new Map(); +type DueNode = { due: number; timers: Timer[] }; +const dueTree = new RBTree((a, b) => a.due - b.due); + +function clearGlobalTimeout(): void { + globalTimeoutDue = null; + stopGlobalTimer(); +} + +let pendingEvents = 0; +const pendingFireTimers: Timer[] = []; +let hasPendingFireTimers = false; +let pendingScheduleTimers: Timer[] = []; + +async function setGlobalTimeout(due: number, now: number): Promise { + // Since JS and Rust don't use the same clock, pass the time to rust as a + // relative time value. On the Rust side we'll turn that into an absolute + // value again. + const timeout = due - now; + assert(timeout >= 0); + // Send message to the backend. + globalTimeoutDue = due; + pendingEvents++; + // FIXME(bartlomieju): this is problematic, because `clearGlobalTimeout` + // is synchronous. That means that timer is cancelled, but this promise is still pending + // until next turn of event loop. This leads to "leaking of async ops" in tests; + // because `clearTimeout/clearInterval` might be the last statement in test function + // `opSanitizer` will immediately complain that there is pending op going on, unless + // some timeout/defer is put in place to allow promise resolution. + // Ideally `clearGlobalTimeout` doesn't return until this op is resolved, but + // I'm not if that's possible. + await startGlobalTimer(timeout); + pendingEvents--; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + fireTimers(); +} + +function setOrClearGlobalTimeout(due: number | null, now: number): void { + if (due == null) { + clearGlobalTimeout(); + } else { + setGlobalTimeout(due, now); + } +} + +function schedule(timer: Timer, now: number): void { + assert(!timer.scheduled); + assert(now <= timer.due); + // There are more timers pending firing. + // We must ensure new timer scheduled after them. + // Push them to a queue that would be depleted after last pending fire + // timer is fired. + // (This also implies behavior of setInterval) + if (hasPendingFireTimers) { + pendingScheduleTimers.push(timer); + return; + } + // Find or create the list of timers that will fire at point-in-time `due`. + const maybeNewDueNode = { due: timer.due, timers: [] }; + let dueNode = dueTree.find(maybeNewDueNode); + if (dueNode === null) { + dueTree.insert(maybeNewDueNode); + dueNode = maybeNewDueNode; + } + // Append the newly scheduled timer to the list and mark it as scheduled. + dueNode!.timers.push(timer); + timer.scheduled = true; + // If the new timer is scheduled to fire before any timer that existed before, + // update the global timeout to reflect this. + if (globalTimeoutDue === null || globalTimeoutDue > timer.due) { + setOrClearGlobalTimeout(timer.due, now); + } +} + +function unschedule(timer: Timer): void { + // Check if our timer is pending scheduling or pending firing. + // If either is true, they are not in tree, and their idMap entry + // will be deleted soon. Remove it from queue. + let index = -1; + if ((index = pendingScheduleTimers.indexOf(timer)) >= 0) { + pendingScheduleTimers.splice(index); + return; + } + if ((index = pendingFireTimers.indexOf(timer)) >= 0) { + pendingFireTimers.splice(index); + return; + } + // If timer is not in the 2 pending queues and is unscheduled, + // it is not in the tree. + if (!timer.scheduled) { + return; + } + const searchKey = { due: timer.due, timers: [] }; + // Find the list of timers that will fire at point-in-time `due`. + const list = dueTree.find(searchKey)!.timers; + if (list.length === 1) { + // Time timer is the only one in the list. Remove the entire list. + assert(list[0] === timer); + dueTree.remove(searchKey); + // If the unscheduled timer was 'next up', find when the next timer that + // still exists is due, and update the global alarm accordingly. + if (timer.due === globalTimeoutDue) { + const nextDueNode: DueNode | null = dueTree.min(); + setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, Date.now()); + } + } else { + // Multiple timers that are due at the same point in time. + // Remove this timer from the list. + const index = list.indexOf(timer); + assert(index > -1); + list.splice(index, 1); + } +} + +function fire(timer: Timer): void { + // If the timer isn't found in the ID map, that means it has been cancelled + // between the timer firing and the promise callback (this function). + if (!idMap.has(timer.id)) { + return; + } + // Reschedule the timer if it is a repeating one, otherwise drop it. + if (!timer.repeat) { + // One-shot timer: remove the timer from this id-to-timer map. + idMap.delete(timer.id); + } else { + // Interval timer: compute when timer was supposed to fire next. + // However make sure to never schedule the next interval in the past. + const now = Date.now(); + timer.due = Math.max(now, timer.due + timer.delay); + schedule(timer, now); + } + // Call the user callback. Intermediate assignment is to avoid leaking `this` + // to it, while also keeping the stack trace neat when it shows up in there. + const callback = timer.callback; + callback(); +} + +function fireTimers(): void { + const now = Date.now(); + // Bail out if we're not expecting the global timer to fire. + if (globalTimeoutDue === null || pendingEvents > 0) { + return; + } + // After firing the timers that are due now, this will hold the first timer + // list that hasn't fired yet. + let nextDueNode: DueNode | null; + while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { + dueTree.remove(nextDueNode); + // Fire all the timers in the list. + for (const timer of nextDueNode.timers) { + // With the list dropped, the timer is no longer scheduled. + timer.scheduled = false; + // Place the callback to pending timers to fire. + pendingFireTimers.push(timer); + } + } + if (pendingFireTimers.length > 0) { + hasPendingFireTimers = true; + // Fire the list of pending timers as a chain of microtasks. + globalThis.queueMicrotask(firePendingTimers); + } else { + setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); + } +} + +function firePendingTimers(): void { + if (pendingFireTimers.length === 0) { + // All timer tasks are done. + hasPendingFireTimers = false; + // Schedule all new timers pushed during previous timer executions + const now = Date.now(); + for (const newTimer of pendingScheduleTimers) { + newTimer.due = Math.max(newTimer.due, now); + schedule(newTimer, now); + } + pendingScheduleTimers = []; + // Reschedule for next round of timeout. + const nextDueNode = dueTree.min(); + const due = nextDueNode && Math.max(nextDueNode.due, now); + setOrClearGlobalTimeout(due, now); + } else { + // Fire a single timer and allow its children microtasks scheduled first. + fire(pendingFireTimers.shift()!); + // ...and we schedule next timer after this. + globalThis.queueMicrotask(firePendingTimers); + } +} + +export type Args = unknown[]; + +function checkThis(thisArg: unknown): void { + if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { + throw new TypeError("Illegal invocation"); + } +} + +function checkBigInt(n: unknown): void { + if (typeof n === "bigint") { + throw new TypeError("Cannot convert a BigInt value to a number"); + } +} + +function setTimer( + cb: (...args: Args) => void, + delay: number, + args: Args, + repeat: boolean +): number { + // Bind `args` to the callback and bind `this` to globalThis(global). + const callback: () => void = cb.bind(globalThis, ...args); + // In the browser, the delay value must be coercible to an integer between 0 + // and INT32_MAX. Any other value will cause the timer to fire immediately. + // We emulate this behavior. + const now = Date.now(); + if (delay > TIMEOUT_MAX) { + console.warn( + `${delay} does not fit into` + + " a 32-bit signed integer." + + "\nTimeout duration was set to 1." + ); + delay = 1; + } + delay = Math.max(0, delay | 0); + + // Create a new, unscheduled timer object. + const timer = { + id: nextTimerId++, + callback, + args, + delay, + due: now + delay, + repeat, + scheduled: false + }; + // Register the timer's existence in the id-to-timer map. + idMap.set(timer.id, timer); + // Schedule the timer in the due table. + schedule(timer, now); + return timer.id; +} + +/** Sets a timer which executes a function once after the timer expires. */ +export function setTimeout( + cb: (...args: Args) => void, + delay = 0, + ...args: Args +): number { + checkBigInt(delay); + // @ts-ignore + checkThis(this); + return setTimer(cb, delay, args, false); +} + +/** Repeatedly calls a function, with a fixed time delay between each call. */ +export function setInterval( + cb: (...args: Args) => void, + delay = 0, + ...args: Args +): number { + checkBigInt(delay); + // @ts-ignore + checkThis(this); + return setTimer(cb, delay, args, true); +} + +/** Clears a previously set timer by id. AKA clearTimeout and clearInterval. */ +function clearTimer(id: number): void { + id = Number(id); + const timer = idMap.get(id); + if (timer === undefined) { + // Timer doesn't exist any more or never existed. This is not an error. + return; + } + // Unschedule the timer if it is currently scheduled, and forget about it. + unschedule(timer); + idMap.delete(timer.id); +} + +export function clearTimeout(id = 0): void { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); +} + +export function clearInterval(id = 0): void { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); +} diff --git a/cli/js/web/url.ts b/cli/js/web/url.ts index 076ec81f1..6ef6b367c 100644 --- a/cli/js/web/url.ts +++ b/cli/js/web/url.ts @@ -2,7 +2,7 @@ import * as urlSearchParams from "./url_search_params.ts"; import * as domTypes from "./dom_types.ts"; import { getRandomValues } from "../ops/get_random_values.ts"; -import { customInspect } from "../console.ts"; +import { customInspect } from "./console.ts"; interface URLParts { protocol: string; diff --git a/cli/js/web/workers.ts b/cli/js/web/workers.ts new file mode 100644 index 000000000..256090d57 --- /dev/null +++ b/cli/js/web/workers.ts @@ -0,0 +1,170 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + createWorker, + hostTerminateWorker, + hostPostMessage, + hostGetMessage +} from "../ops/worker_host.ts"; +import { log } from "../util.ts"; +import { TextDecoder, TextEncoder } from "./text_encoding.ts"; +/* +import { blobURLMap } from "./web/url.ts"; +import { blobBytesWeakMap } from "./web/blob.ts"; +*/ +import { Event } from "./event.ts"; +import { EventTarget } from "./event_target.ts"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function encodeMessage(data: any): Uint8Array { + const dataJson = JSON.stringify(data); + return encoder.encode(dataJson); +} + +function decodeMessage(dataIntArray: Uint8Array): any { + const dataJson = decoder.decode(dataIntArray); + return JSON.parse(dataJson); +} + +interface WorkerEvent { + event: "error" | "msg" | "close"; + data?: any; + error?: any; +} + +export interface Worker { + onerror?: (e: any) => void; + onmessage?: (e: { data: any }) => void; + onmessageerror?: () => void; + postMessage(data: any): void; + terminate(): void; +} + +export interface WorkerOptions { + type?: "classic" | "module"; + name?: string; +} + +export class WorkerImpl extends EventTarget implements Worker { + private readonly id: number; + private isClosing = false; + public onerror?: (e: any) => void; + public onmessage?: (data: any) => void; + public onmessageerror?: () => void; + private name: string; + private terminated = false; + + constructor(specifier: string, options?: WorkerOptions) { + super(); + const { type = "classic", name = "unknown" } = options ?? {}; + + if (type !== "module") { + throw new Error( + 'Not yet implemented: only "module" type workers are supported' + ); + } + + this.name = name; + const hasSourceCode = false; + const sourceCode = decoder.decode(new Uint8Array()); + + /* TODO(bartlomieju): + // Handle blob URL. + if (specifier.startsWith("blob:")) { + hasSourceCode = true; + const b = blobURLMap.get(specifier); + if (!b) { + throw new Error("No Blob associated with the given URL is found"); + } + const blobBytes = blobBytesWeakMap.get(b!); + if (!blobBytes) { + throw new Error("Invalid Blob"); + } + sourceCode = blobBytes!; + } + */ + + const { id } = createWorker( + specifier, + hasSourceCode, + sourceCode, + options?.name + ); + this.id = id; + this.poll(); + } + + private handleError(e: any): boolean { + // TODO: this is being handled in a type unsafe way, it should be type safe + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const event = new Event("error", { cancelable: true }) as any; + event.message = e.message; + event.lineNumber = e.lineNumber ? e.lineNumber + 1 : null; + event.columnNumber = e.columnNumber ? e.columnNumber + 1 : null; + event.fileName = e.fileName; + event.error = null; + + let handled = false; + if (this.onerror) { + this.onerror(event); + if (event.defaultPrevented) { + handled = true; + } + } + + return handled; + } + + async poll(): Promise { + while (!this.terminated) { + const event = await hostGetMessage(this.id); + + // If terminate was called then we ignore all messages + if (this.terminated) { + return; + } + + const type = event.type; + + if (type === "msg") { + if (this.onmessage) { + const message = decodeMessage(new Uint8Array(event.data)); + this.onmessage({ data: message }); + } + continue; + } + + if (type === "error") { + if (!this.handleError(event.error)) { + throw Error(event.error.message); + } + continue; + } + + if (type === "close") { + log(`Host got "close" message from worker: ${this.name}`); + this.terminated = true; + return; + } + + throw new Error(`Unknown worker event: "${type}"`); + } + } + + postMessage(data: any): void { + if (this.terminated) { + return; + } + + hostPostMessage(this.id, encodeMessage(data)); + } + + terminate(): void { + if (!this.terminated) { + this.terminated = true; + hostTerminateWorker(this.id); + } + } +} diff --git a/cli/js/workers.ts b/cli/js/workers.ts deleted file mode 100644 index 818c0ecf4..000000000 --- a/cli/js/workers.ts +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - createWorker, - hostTerminateWorker, - hostPostMessage, - hostGetMessage -} from "./ops/worker_host.ts"; -import { log } from "./util.ts"; -import { TextDecoder, TextEncoder } from "./web/text_encoding.ts"; -/* -import { blobURLMap } from "./web/url.ts"; -import { blobBytesWeakMap } from "./web/blob.ts"; -*/ -import { Event } from "./web/event.ts"; -import { EventTarget } from "./web/event_target.ts"; - -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); - -function encodeMessage(data: any): Uint8Array { - const dataJson = JSON.stringify(data); - return encoder.encode(dataJson); -} - -function decodeMessage(dataIntArray: Uint8Array): any { - const dataJson = decoder.decode(dataIntArray); - return JSON.parse(dataJson); -} - -interface WorkerEvent { - event: "error" | "msg" | "close"; - data?: any; - error?: any; -} - -export interface Worker { - onerror?: (e: any) => void; - onmessage?: (e: { data: any }) => void; - onmessageerror?: () => void; - postMessage(data: any): void; - terminate(): void; -} - -export interface WorkerOptions { - type?: "classic" | "module"; - name?: string; -} - -export class WorkerImpl extends EventTarget implements Worker { - private readonly id: number; - private isClosing = false; - public onerror?: (e: any) => void; - public onmessage?: (data: any) => void; - public onmessageerror?: () => void; - private name: string; - private terminated = false; - - constructor(specifier: string, options?: WorkerOptions) { - super(); - const { type = "classic", name = "unknown" } = options ?? {}; - - if (type !== "module") { - throw new Error( - 'Not yet implemented: only "module" type workers are supported' - ); - } - - this.name = name; - const hasSourceCode = false; - const sourceCode = decoder.decode(new Uint8Array()); - - /* TODO(bartlomieju): - // Handle blob URL. - if (specifier.startsWith("blob:")) { - hasSourceCode = true; - const b = blobURLMap.get(specifier); - if (!b) { - throw new Error("No Blob associated with the given URL is found"); - } - const blobBytes = blobBytesWeakMap.get(b!); - if (!blobBytes) { - throw new Error("Invalid Blob"); - } - sourceCode = blobBytes!; - } - */ - - const { id } = createWorker( - specifier, - hasSourceCode, - sourceCode, - options?.name - ); - this.id = id; - this.poll(); - } - - private handleError(e: any): boolean { - // TODO: this is being handled in a type unsafe way, it should be type safe - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const event = new Event("error", { cancelable: true }) as any; - event.message = e.message; - event.lineNumber = e.lineNumber ? e.lineNumber + 1 : null; - event.columnNumber = e.columnNumber ? e.columnNumber + 1 : null; - event.fileName = e.fileName; - event.error = null; - - let handled = false; - if (this.onerror) { - this.onerror(event); - if (event.defaultPrevented) { - handled = true; - } - } - - return handled; - } - - async poll(): Promise { - while (!this.terminated) { - const event = await hostGetMessage(this.id); - - // If terminate was called then we ignore all messages - if (this.terminated) { - return; - } - - const type = event.type; - - if (type === "msg") { - if (this.onmessage) { - const message = decodeMessage(new Uint8Array(event.data)); - this.onmessage({ data: message }); - } - continue; - } - - if (type === "error") { - if (!this.handleError(event.error)) { - throw Error(event.error.message); - } - continue; - } - - if (type === "close") { - log(`Host got "close" message from worker: ${this.name}`); - this.terminated = true; - return; - } - - throw new Error(`Unknown worker event: "${type}"`); - } - } - - postMessage(data: any): void { - if (this.terminated) { - return; - } - - hostPostMessage(this.id, encodeMessage(data)); - } - - terminate(): void { - if (!this.terminated) { - this.terminated = true; - hostTerminateWorker(this.id); - } - } -} -- cgit v1.2.3