summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BUILD.gn4
-rw-r--r--js/assets.ts2
-rw-r--r--js/compiler.ts595
-rw-r--r--js/compiler_test.ts471
-rw-r--r--js/globals.ts25
-rw-r--r--js/main.ts14
-rw-r--r--js/runtime.ts376
-rw-r--r--js/tsconfig.generated.json1
-rw-r--r--js/unit_tests.ts2
-rw-r--r--tests/013_dynamic_import.ts17
-rw-r--r--tests/013_dynamic_import.ts.out1
-rw-r--r--tests/async_error.ts.out5
-rw-r--r--tests/error_001.ts.out5
-rw-r--r--tests/error_002.ts.out7
-rw-r--r--tests/error_003_typescript.ts2
-rw-r--r--tests/error_003_typescript.ts.out10
-rw-r--r--tests/error_004_missing_module.ts1
-rw-r--r--tests/error_004_missing_module.ts.out12
18 files changed, 1154 insertions, 396 deletions
diff --git a/BUILD.gn b/BUILD.gn
index 38b194b9a..c7251981d 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -162,6 +162,7 @@ executable("snapshot_creator") {
run_node("gen_declarations") {
out_dir = target_gen_dir
sources = [
+ "js/compiler.ts",
"js/console.ts",
"js/deno.ts",
"js/globals.ts",
@@ -171,6 +172,7 @@ run_node("gen_declarations") {
"js/util.ts",
]
outputs = [
+ out_dir + "/js/compiler.d.ts",
out_dir + "/js/console.d.ts",
out_dir + "/js/deno.d.ts",
out_dir + "/js/globals.d.ts",
@@ -196,6 +198,7 @@ run_node("bundle") {
out_dir = "$target_gen_dir/bundle/"
sources = [
"js/assets.ts",
+ "js/compiler.ts",
"js/console.ts",
"js/fetch.ts",
"js/fetch_types.d.ts",
@@ -204,7 +207,6 @@ run_node("bundle") {
"js/main.ts",
"js/os.ts",
"js/plugins.d.ts",
- "js/runtime.ts",
"js/text_encoding.ts",
"js/timers.ts",
"js/types.d.ts",
diff --git a/js/assets.ts b/js/assets.ts
index 425ef3fc2..3b5691bc7 100644
--- a/js/assets.ts
+++ b/js/assets.ts
@@ -7,6 +7,7 @@
// tslint:disable:max-line-length
// Generated definitions
+import compilerDts from "gen/js/compiler.d.ts!string";
import consoleDts from "gen/js/console.d.ts!string";
import denoDts from "gen/js/deno.d.ts!string";
import globalsDts from "gen/js/globals.d.ts!string";
@@ -55,6 +56,7 @@ import fetchTypesDts from "/js/fetch_types.d.ts!string";
// prettier-ignore
export const assetSourceCode: { [key: string]: string } = {
// Generated definitions
+ "compiler.d.ts": compilerDts,
"console.d.ts": consoleDts,
"deno.d.ts": denoDts,
"globals.d.ts": globalsDts,
diff --git a/js/compiler.ts b/js/compiler.ts
new file mode 100644
index 000000000..84330490f
--- /dev/null
+++ b/js/compiler.ts
@@ -0,0 +1,595 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import * as ts from "typescript";
+import { assetSourceCode } from "./assets";
+import * as deno from "./deno";
+import { libdeno, window, globalEval } from "./globals";
+import * as os from "./os";
+import { RawSourceMap } from "./types";
+import { assert, log, notImplemented } from "./util";
+import * as sourceMaps from "./v8_source_maps";
+
+const EOL = "\n";
+const ASSETS = "$asset$";
+
+// tslint:disable:no-any
+type AmdCallback = (...args: any[]) => void;
+type AmdErrback = (err: any) => void;
+export type AmdFactory = (...args: any[]) => object | void;
+// tslint:enable:no-any
+export type AmdDefine = (deps: string[], factory: AmdFactory) => void;
+type AmdRequire = (
+ deps: string[],
+ callback: AmdCallback,
+ errback?: AmdErrback
+) => void;
+
+// The location that a module is being loaded from. This could be a directory,
+// like ".", or it could be a module specifier like
+// "http://gist.github.com/somefile.ts"
+type ContainingFile = string;
+// The internal local filename of a compiled module. It will often be something
+// like "/home/ry/.deno/gen/f7b4605dfbc4d3bb356e98fda6ceb1481e4a8df5.js"
+type ModuleFileName = string;
+// The external name of a module - could be a URL or could be a relative path.
+// Examples "http://gist.github.com/somefile.ts" or "./somefile.ts"
+type ModuleSpecifier = string;
+// The compiled source code which is cached in .deno/gen/
+type OutputCode = string;
+
+/**
+ * Abstraction of the APIs required from the `os` module so they can be
+ * easily mocked.
+ */
+export interface Os {
+ codeCache: typeof os.codeCache;
+ codeFetch: typeof os.codeFetch;
+ exit: typeof os.exit;
+}
+
+/**
+ * Abstraction of the APIs required from the `typescript` module so they can
+ * be easily mocked.
+ */
+export interface Ts {
+ createLanguageService: typeof ts.createLanguageService;
+ /* tslint:disable-next-line:max-line-length */
+ formatDiagnosticsWithColorAndContext: typeof ts.formatDiagnosticsWithColorAndContext;
+}
+
+/**
+ * A simple object structure for caching resolved modules and their contents.
+ *
+ * Named `ModuleMetaData` to clarify it is just a representation of meta data of
+ * the module, not the actual module instance.
+ */
+export class ModuleMetaData {
+ public readonly exports = {};
+ public scriptSnapshot?: ts.IScriptSnapshot;
+ public scriptVersion = "";
+
+ constructor(
+ public readonly fileName: string,
+ public readonly sourceCode = "",
+ public outputCode = ""
+ ) {
+ if (outputCode !== "" || fileName.endsWith(".d.ts")) {
+ this.scriptVersion = "1";
+ }
+ }
+}
+
+/**
+ * The required minimal API to allow formatting of TypeScript compiler
+ * diagnostics.
+ */
+const formatDiagnosticsHost: ts.FormatDiagnosticsHost = {
+ getCurrentDirectory: () => ".",
+ getCanonicalFileName: (fileName: string) => fileName,
+ getNewLine: () => EOL
+};
+
+/**
+ * Throw a module resolution error, when a module is unsuccessfully resolved.
+ */
+function throwResolutionError(
+ message: string,
+ moduleSpecifier: ModuleSpecifier,
+ containingFile: ContainingFile
+): never {
+ throw new Error(
+ // tslint:disable-next-line:max-line-length
+ `Cannot resolve module "${moduleSpecifier}" from "${containingFile}".\n ${message}`
+ );
+}
+
+// ts.ScriptKind is not available at runtime, so local enum definition
+enum ScriptKind {
+ JS = 1,
+ TS = 3,
+ JSON = 6
+}
+
+/**
+ * A singleton class that combines the TypeScript Language Service host API
+ * with Deno specific APIs to provide an interface for compiling and running
+ * TypeScript and JavaScript modules.
+ */
+export class DenoCompiler implements ts.LanguageServiceHost {
+ // Modules are usually referenced by their ModuleSpecifier and ContainingFile,
+ // and keeping a map of the resolved module file name allows more efficient
+ // future resolution
+ private readonly _fileNamesMap = new Map<
+ ContainingFile,
+ Map<ModuleSpecifier, ModuleFileName>
+ >();
+ // A reference to global eval, so it can be monkey patched during testing
+ private _globalEval = globalEval;
+ // A reference to the log utility, so it can be monkey patched during testing
+ private _log = log;
+ // A map of module file names to module meta data
+ private readonly _moduleMetaDataMap = new Map<
+ ModuleFileName,
+ ModuleMetaData
+ >();
+ // TODO ideally this are not static and can be influenced by command line
+ // arguments
+ private readonly _options: Readonly<ts.CompilerOptions> = {
+ allowJs: true,
+ module: ts.ModuleKind.AMD,
+ outDir: "$deno$",
+ // TODO https://github.com/denoland/deno/issues/23
+ inlineSourceMap: true,
+ inlineSources: true,
+ stripComments: true,
+ target: ts.ScriptTarget.ESNext
+ };
+ // A reference to the `./os.ts` module, so it can be monkey patched during
+ // testing
+ private _os: Os = os;
+ // Used to contain the script file we are currently running
+ private _scriptFileNames: string[] = [];
+ // A reference to the TypeScript LanguageService instance so it can be
+ // monkey patched during testing
+ private _service: ts.LanguageService;
+ // A reference to `typescript` module so it can be monkey patched during
+ // testing
+ private _ts: Ts = ts;
+ // A reference to the global scope so it can be monkey patched during
+ // testing
+ private _window = window;
+
+ /**
+ * The TypeScript language service often refers to the resolved fileName of
+ * a module, this is a shortcut to avoid unnecessary module resolution logic
+ * for modules that may have been initially resolved by a `moduleSpecifier`
+ * and `containingFile`. Also, `resolveModule()` throws when the module
+ * cannot be resolved, which isn't always valid when dealing with the
+ * TypeScript compiler, but the TypeScript compiler shouldn't be asking about
+ * external modules that we haven't told it about yet.
+ */
+ private _getModuleMetaData(
+ fileName: ModuleFileName
+ ): ModuleMetaData | undefined {
+ return this._moduleMetaDataMap.has(fileName)
+ ? this._moduleMetaDataMap.get(fileName)
+ : fileName.startsWith(ASSETS)
+ ? this.resolveModule(fileName, "")
+ : undefined;
+ }
+
+ /**
+ * Setup being able to map back source references back to their source
+ *
+ * TODO is this the best place for this? It is tightly coupled to how the
+ * compiler works, but it is also tightly coupled to how the whole runtime
+ * environment is bootstrapped. It also needs efficient access to the
+ * `outputCode` of the module information, which exists inside of the
+ * compiler instance.
+ */
+ private _setupSourceMaps(): void {
+ sourceMaps.install({
+ installPrepareStackTrace: true,
+ getGeneratedContents: (fileName: string): string | RawSourceMap => {
+ this._log("getGeneratedContents", fileName);
+ if (fileName === "gen/bundle/main.js") {
+ assert(libdeno.mainSource.length > 0);
+ return libdeno.mainSource;
+ } else if (fileName === "main.js.map") {
+ return libdeno.mainSourceMap;
+ } else if (fileName === "deno_main.js") {
+ return "";
+ } else {
+ const moduleMetaData = this._moduleMetaDataMap.get(fileName);
+ if (!moduleMetaData) {
+ this._log("getGeneratedContents cannot find", fileName);
+ return "";
+ }
+ return moduleMetaData.outputCode;
+ }
+ }
+ });
+ }
+
+ private constructor() {
+ if (DenoCompiler._instance) {
+ throw new TypeError("Attempt to create an additional compiler.");
+ }
+ this._service = this._ts.createLanguageService(this);
+ this._setupSourceMaps();
+ }
+
+ // Deno specific compiler API
+
+ /**
+ * Retrieve the output of the TypeScript compiler for a given `fileName`.
+ */
+ compile(fileName: ModuleFileName): OutputCode {
+ const service = this._service;
+ const output = service.getEmitOutput(fileName);
+
+ // Get the relevant diagnostics - this is 3x faster than
+ // `getPreEmitDiagnostics`.
+ const diagnostics = [
+ ...service.getCompilerOptionsDiagnostics(),
+ ...service.getSyntacticDiagnostics(fileName),
+ ...service.getSemanticDiagnostics(fileName)
+ ];
+ if (diagnostics.length > 0) {
+ const errMsg = this._ts.formatDiagnosticsWithColorAndContext(
+ diagnostics,
+ formatDiagnosticsHost
+ );
+ console.log(errMsg);
+ // All TypeScript errors are terminal for deno
+ this._os.exit(1);
+ }
+
+ assert(!output.emitSkipped, "The emit was skipped for an unknown reason.");
+
+ // Currently we are inlining source maps, there should be only 1 output file
+ // See: https://github.com/denoland/deno/issues/23
+ assert(
+ output.outputFiles.length === 1,
+ "Only single file should be output."
+ );
+
+ const [outputFile] = output.outputFiles;
+ return outputFile.text;
+ }
+
+ /**
+ * Create a localized AMD `define` function and return it.
+ */
+ makeDefine(moduleMetaData: ModuleMetaData): AmdDefine {
+ const localDefine = (deps: string[], factory: AmdFactory): void => {
+ // TypeScript will emit a local require dependency when doing dynamic
+ // `import()`
+ const localRequire: AmdRequire = (
+ deps: string[],
+ callback: AmdCallback,
+ errback?: AmdErrback
+ ): void => {
+ this._log("localRequire", deps);
+ try {
+ const args = deps.map(dep => {
+ if (dep in DenoCompiler._builtins) {
+ return DenoCompiler._builtins[dep];
+ } else {
+ const depModuleMetaData = this.run(dep, moduleMetaData.fileName);
+ return depModuleMetaData.exports;
+ }
+ });
+ callback(...args);
+ } catch (e) {
+ if (errback) {
+ errback(e);
+ } else {
+ throw e;
+ }
+ }
+ };
+ const localExports = moduleMetaData.exports;
+ this._log("localDefine", moduleMetaData.fileName, deps, localExports);
+ const args = deps.map(dep => {
+ if (dep === "require") {
+ return localRequire;
+ } else if (dep === "exports") {
+ return localExports;
+ } else if (dep in DenoCompiler._builtins) {
+ return DenoCompiler._builtins[dep];
+ } else {
+ const depModuleMetaData = this.run(dep, moduleMetaData.fileName);
+ return depModuleMetaData.exports;
+ }
+ });
+ factory(...args);
+ };
+ return localDefine;
+ }
+
+ /**
+ * Given a `moduleSpecifier` and `containingFile` retrieve the cached
+ * `fileName` for a given module. If the module has yet to be resolved
+ * this will return `undefined`.
+ */
+ resolveFileName(
+ moduleSpecifier: ModuleSpecifier,
+ containingFile: ContainingFile
+ ): ModuleFileName | undefined {
+ this._log("resolveFileName", { moduleSpecifier, containingFile });
+ const innerMap = this._fileNamesMap.get(containingFile);
+ if (innerMap) {
+ return innerMap.get(moduleSpecifier);
+ }
+ return undefined;
+ }
+
+ /**
+ * Given a `moduleSpecifier` and `containingFile`, resolve the module and
+ * return the `ModuleMetaData`.
+ */
+ resolveModule(
+ moduleSpecifier: ModuleSpecifier,
+ containingFile: ContainingFile
+ ): ModuleMetaData {
+ this._log("resolveModule", { moduleSpecifier, containingFile });
+ assert(moduleSpecifier != null && moduleSpecifier.length > 0);
+ let fileName = this.resolveFileName(moduleSpecifier, containingFile);
+ if (fileName && this._moduleMetaDataMap.has(fileName)) {
+ return this._moduleMetaDataMap.get(fileName)!;
+ }
+ let sourceCode: string | undefined;
+ let outputCode: string | undefined;
+ if (
+ moduleSpecifier.startsWith(ASSETS) ||
+ containingFile.startsWith(ASSETS)
+ ) {
+ // Assets are compiled into the runtime javascript bundle.
+ // we _know_ `.pop()` will return a string, but TypeScript doesn't so
+ // not null assertion
+ const moduleId = moduleSpecifier.split("/").pop()!;
+ const assetName = moduleId.includes(".") ? moduleId : `${moduleId}.d.ts`;
+ assert(assetName in assetSourceCode, `No such asset "${assetName}"`);
+ sourceCode = assetSourceCode[assetName];
+ fileName = `${ASSETS}/${assetName}`;
+ } else {
+ // We query Rust with a CodeFetch message. It will load the sourceCode,
+ // and if there is any outputCode cached, will return that as well.
+ let fetchResponse;
+ try {
+ fetchResponse = this._os.codeFetch(moduleSpecifier, containingFile);
+ } catch (e) {
+ return throwResolutionError(
+ `os.codeFetch message: ${e.message}`,
+ moduleSpecifier,
+ containingFile
+ );
+ }
+ fileName = fetchResponse.filename || undefined;
+ sourceCode = fetchResponse.sourceCode || undefined;
+ outputCode = fetchResponse.outputCode || undefined;
+ }
+ if (!sourceCode || sourceCode.length === 0 || !fileName) {
+ return throwResolutionError(
+ "Invalid source code or file name.",
+ moduleSpecifier,
+ containingFile
+ );
+ }
+ this._log("resolveModule sourceCode length ", sourceCode.length);
+ this.setFileName(moduleSpecifier, containingFile, fileName);
+ if (fileName && this._moduleMetaDataMap.has(fileName)) {
+ return this._moduleMetaDataMap.get(fileName)!;
+ }
+ const moduleMetaData = new ModuleMetaData(fileName, sourceCode, outputCode);
+ this._moduleMetaDataMap.set(fileName, moduleMetaData);
+ return moduleMetaData;
+ }
+
+ /**
+ * Resolve the `fileName` for a given `moduleSpecifier` and `containingFile`
+ */
+ resolveModuleName(
+ moduleSpecifier: ModuleSpecifier,
+ containingFile: ContainingFile
+ ): ModuleFileName | undefined {
+ const moduleMetaData = this.resolveModule(moduleSpecifier, containingFile);
+ return moduleMetaData ? moduleMetaData.fileName : undefined;
+ }
+
+ /* tslint:disable-next-line:no-any */
+ /**
+ * Execute a module based on the `moduleSpecifier` and the `containingFile`
+ * and return the resulting `FileModule`.
+ */
+ run(
+ moduleSpecifier: ModuleSpecifier,
+ containingFile: ContainingFile
+ ): ModuleMetaData {
+ this._log("run", { moduleSpecifier, containingFile });
+ const moduleMetaData = this.resolveModule(moduleSpecifier, containingFile);
+ const fileName = moduleMetaData.fileName;
+ this._scriptFileNames = [fileName];
+ const sourceCode = moduleMetaData.sourceCode;
+ let outputCode = moduleMetaData.outputCode;
+ if (!outputCode) {
+ outputCode = moduleMetaData.outputCode = `${this.compile(
+ fileName
+ )}\n//# sourceURL=${fileName}`;
+ moduleMetaData!.scriptVersion = "1";
+ this._os.codeCache(fileName, sourceCode, outputCode);
+ }
+ this._window.define = this.makeDefine(moduleMetaData);
+ this._globalEval(moduleMetaData.outputCode);
+ this._window.define = undefined;
+ return moduleMetaData!;
+ }
+
+ /**
+ * Caches the resolved `fileName` in relationship to the `moduleSpecifier`
+ * and `containingFile` in order to reduce calls to the privileged side
+ * to retrieve the contents of a module.
+ */
+ setFileName(
+ moduleSpecifier: ModuleSpecifier,
+ containingFile: ContainingFile,
+ fileName: ModuleFileName
+ ): void {
+ this._log("setFileName", { moduleSpecifier, containingFile });
+ let innerMap = this._fileNamesMap.get(containingFile);
+ if (!innerMap) {
+ innerMap = new Map();
+ this._fileNamesMap.set(containingFile, innerMap);
+ }
+ innerMap.set(moduleSpecifier, fileName);
+ }
+
+ // TypeScript Language Service API
+
+ getCompilationSettings(): ts.CompilerOptions {
+ this._log("getCompilationSettings()");
+ return this._options;
+ }
+
+ getNewLine(): string {
+ return EOL;
+ }
+
+ getScriptFileNames(): string[] {
+ // This is equal to `"files"` in the `tsconfig.json`, therefore we only need
+ // to include the actual base source files we are evaluating at the moment,
+ // which would be what is set during the `.run()`
+ return this._scriptFileNames;
+ }
+
+ getScriptKind(fileName: ModuleFileName): ts.ScriptKind {
+ this._log("getScriptKind()", fileName);
+ const suffix = fileName.substr(fileName.lastIndexOf(".") + 1);
+ switch (suffix) {
+ case "ts":
+ return ScriptKind.TS;
+ case "js":
+ return ScriptKind.JS;
+ case "json":
+ return ScriptKind.JSON;
+ default:
+ return this._options.allowJs ? ScriptKind.JS : ScriptKind.TS;
+ }
+ }
+
+ getScriptVersion(fileName: ModuleFileName): string {
+ this._log("getScriptVersion()", fileName);
+ const moduleMetaData = this._getModuleMetaData(fileName);
+ return (moduleMetaData && moduleMetaData.scriptVersion) || "";
+ }
+
+ getScriptSnapshot(fileName: ModuleFileName): ts.IScriptSnapshot | undefined {
+ this._log("getScriptSnapshot()", fileName);
+ const moduleMetaData = this._getModuleMetaData(fileName);
+ if (moduleMetaData) {
+ return (
+ moduleMetaData.scriptSnapshot ||
+ (moduleMetaData.scriptSnapshot = {
+ getText(start, end) {
+ return moduleMetaData.sourceCode.substring(start, end);
+ },
+ getLength() {
+ return moduleMetaData.sourceCode.length;
+ },
+ getChangeRange() {
+ return undefined;
+ }
+ })
+ );
+ } else {
+ return undefined;
+ }
+ }
+
+ getCurrentDirectory(): string {
+ this._log("getCurrentDirectory()");
+ return "";
+ }
+
+ getDefaultLibFileName(): string {
+ this._log("getDefaultLibFileName()");
+ const moduleSpecifier = "lib.globals.d.ts";
+ const moduleMetaData = this.resolveModule(moduleSpecifier, ASSETS);
+ return moduleMetaData.fileName;
+ }
+
+ useCaseSensitiveFileNames(): boolean {
+ this._log("useCaseSensitiveFileNames");
+ return true;
+ }
+
+ readFile(path: string): string | undefined {
+ this._log("readFile", path);
+ return notImplemented();
+ }
+
+ fileExists(fileName: string): boolean {
+ const moduleMetaData = this._getModuleMetaData(fileName);
+ const exists = moduleMetaData != null;
+ this._log("fileExists", fileName, exists);
+ return exists;
+ }
+
+ resolveModuleNames(
+ moduleNames: ModuleSpecifier[],
+ containingFile: ContainingFile
+ ): ts.ResolvedModule[] {
+ this._log("resolveModuleNames", { moduleNames, containingFile });
+ return moduleNames.map(name => {
+ let resolvedFileName;
+ if (name === "deno") {
+ resolvedFileName = this.resolveModuleName("deno.d.ts", ASSETS);
+ } else if (name === "compiler") {
+ resolvedFileName = this.resolveModuleName("compiler.d.ts", ASSETS);
+ } else if (name === "typescript") {
+ resolvedFileName = this.resolveModuleName("typescript.d.ts", ASSETS);
+ } else {
+ resolvedFileName = this.resolveModuleName(name, containingFile);
+ }
+ // According to the interface we shouldn't return `undefined` but if we
+ // fail to return the same length of modules to those we cannot resolve
+ // then TypeScript fails on an assertion that the lengths can't be
+ // different, so we have to return an "empty" resolved module
+ // TODO: all this does is push the problem downstream, and TypeScript
+ // will complain it can't identify the type of the file and throw
+ // a runtime exception, so we need to handle missing modules better
+ resolvedFileName = resolvedFileName || "";
+ // This flags to the compiler to not go looking to transpile functional
+ // code, anything that is in `/$asset$/` is just library code
+ const isExternalLibraryImport = resolvedFileName.startsWith(ASSETS);
+ // TODO: we should be returning a ts.ResolveModuleFull
+ return { resolvedFileName, isExternalLibraryImport };
+ });
+ }
+
+ // Deno specific static properties and methods
+
+ /**
+ * Built in modules which can be returned to external modules
+ *
+ * Placed as a private static otherwise we get use before
+ * declared with the `DenoCompiler`
+ */
+ // tslint:disable-next-line:no-any
+ private static _builtins: { [mid: string]: any } = {
+ typescript: ts,
+ deno,
+ compiler: { DenoCompiler, ModuleMetaData }
+ };
+
+ private static _instance: DenoCompiler | undefined;
+
+ /**
+ * Returns the instance of `DenoCompiler` or creates a new instance.
+ */
+ static instance(): DenoCompiler {
+ return (
+ DenoCompiler._instance || (DenoCompiler._instance = new DenoCompiler())
+ );
+ }
+}
diff --git a/js/compiler_test.ts b/js/compiler_test.ts
new file mode 100644
index 000000000..d8861e2a9
--- /dev/null
+++ b/js/compiler_test.ts
@@ -0,0 +1,471 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEqual } from "./testing/testing.ts";
+import * as compiler from "compiler";
+import * as ts from "typescript";
+
+// We use a silly amount of `any` in these tests...
+// tslint:disable:no-any
+
+const { DenoCompiler, ModuleMetaData } = compiler;
+
+// Enums like this don't exist at runtime, so local copy
+enum ScriptKind {
+ JS = 1,
+ TS = 3,
+ JSON = 6
+}
+
+interface ModuleInfo {
+ moduleName: string | null;
+ filename: string | null;
+ sourceCode: string | null;
+ outputCode: string | null;
+}
+
+const compilerInstance = DenoCompiler.instance();
+
+// References to orignal items we are going to mock
+const originals = {
+ _globalEval: (compilerInstance as any)._globalEval,
+ _log: (compilerInstance as any)._log,
+ _os: (compilerInstance as any)._os,
+ _ts: (compilerInstance as any)._ts,
+ _service: (compilerInstance as any)._service,
+ _window: (compilerInstance as any)._window
+};
+
+function mockModuleInfo(
+ moduleName: string | null,
+ filename: string | null,
+ sourceCode: string | null,
+ outputCode: string | null
+): ModuleInfo {
+ return {
+ moduleName,
+ filename,
+ sourceCode,
+ outputCode
+ };
+}
+
+// Some fixtures we will us in testing
+const fooBarTsSource = `import * as compiler from "compiler";
+console.log(compiler);
+export const foo = "bar";
+`;
+
+const fooBazTsSource = `import { foo } from "./bar.ts";
+console.log(foo);
+`;
+
+// TODO(#23) Remove source map strings from fooBarTsOutput.
+// tslint:disable:max-line-length
+const fooBarTsOutput = `define(["require", "exports", "compiler"], function (require, exports, compiler) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ console.log(compiler);
+ exports.foo = "bar";
+});
+//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZmlsZTovLy9yb290L3Byb2plY3QvZm9vL2Jhci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7SUFDQSxPQUFPLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ1QsUUFBQSxHQUFHLEdBQUcsS0FBSyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgY29tcGlsZXIgZnJvbSBcImNvbXBpbGVyXCI7XG5jb25zb2xlLmxvZyhjb21waWxlcik7XG5leHBvcnQgY29uc3QgZm9vID0gXCJiYXJcIjtcbiJdfQ==
+//# sourceURL=/root/project/foo/bar.ts`;
+
+// TODO(#23) Remove source map strings from fooBazTsOutput.
+const fooBazTsOutput = `define(["require", "exports", "./bar.ts"], function (require, exports, bar_ts_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ console.log(bar_ts_1.foo);
+});
+//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmF6LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZmlsZTovLy9yb290L3Byb2plY3QvZm9vL2Jhei50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7SUFDQSxPQUFPLENBQUMsR0FBRyxDQUFDLFlBQUcsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgZm9vIH0gZnJvbSBcIi4vYmFyLnRzXCI7XG5jb25zb2xlLmxvZyhmb28pO1xuIl19
+//# sourceURL=/root/project/foo/baz.ts`;
+// tslint:enable:max-line-length
+
+const moduleMap: {
+ [containFile: string]: { [moduleSpecifier: string]: ModuleInfo };
+} = {
+ "/root/project": {
+ "foo/bar.ts": mockModuleInfo(
+ "foo/bar",
+ "/root/project/foo/bar.ts",
+ fooBarTsSource,
+ null
+ ),
+ "foo/baz.ts": mockModuleInfo(
+ "foo/baz",
+ "/root/project/foo/baz.ts",
+ fooBazTsSource,
+ null
+ ),
+ "foo/qat.ts": mockModuleInfo(
+ "foo/qat",
+ "/root/project/foo/qat.ts",
+ null,
+ null
+ )
+ },
+ "/root/project/foo/baz.ts": {
+ "./bar.ts": mockModuleInfo(
+ "foo/bar",
+ "/root/project/foo/bar.ts",
+ fooBarTsSource,
+ fooBarTsOutput
+ )
+ }
+};
+
+const emittedFiles = {
+ "/root/project/foo/qat.ts": "console.log('foo');"
+};
+
+let globalEvalStack: string[] = [];
+let getEmitOutputStack: string[] = [];
+let logStack: any[][] = [];
+let codeCacheStack: Array<{
+ fileName: string;
+ sourceCode: string;
+ outputCode: string;
+}> = [];
+let codeFetchStack: Array<{
+ moduleSpecifier: string;
+ containingFile: string;
+}> = [];
+
+function reset() {
+ codeFetchStack = [];
+ codeCacheStack = [];
+ logStack = [];
+ getEmitOutputStack = [];
+ globalEvalStack = [];
+}
+
+let mockDeps: string[] | undefined;
+let mockFactory: compiler.AmdFactory;
+
+function globalEvalMock(x: string): void {
+ globalEvalStack.push(x);
+ if (windowMock.define && mockDeps && mockFactory) {
+ windowMock.define(mockDeps, mockFactory);
+ }
+}
+function logMock(...args: any[]): void {
+ logStack.push(args);
+}
+const osMock: compiler.Os = {
+ codeCache(fileName: string, sourceCode: string, outputCode: string): void {
+ codeCacheStack.push({ fileName, sourceCode, outputCode });
+ },
+ codeFetch(moduleSpecifier: string, containingFile: string): ModuleInfo {
+ codeFetchStack.push({ moduleSpecifier, containingFile });
+ if (containingFile in moduleMap) {
+ if (moduleSpecifier in moduleMap[containingFile]) {
+ return moduleMap[containingFile][moduleSpecifier];
+ }
+ }
+ return mockModuleInfo(null, null, null, null);
+ },
+ exit(code: number): never {
+ throw new Error(`os.exit(${code})`);
+ }
+};
+const tsMock: compiler.Ts = {
+ createLanguageService(host: ts.LanguageServiceHost): ts.LanguageService {
+ return {} as ts.LanguageService;
+ },
+ formatDiagnosticsWithColorAndContext(
+ diagnostics: ReadonlyArray<ts.Diagnostic>,
+ host: ts.FormatDiagnosticsHost
+ ): string {
+ return "";
+ }
+};
+
+const getEmitOutputPassThrough = true;
+
+const serviceMock = {
+ getCompilerOptionsDiagnostics(): ts.Diagnostic[] {
+ return originals._service.getCompilerOptionsDiagnostics.call(
+ originals._service
+ );
+ },
+ getEmitOutput(fileName: string): ts.EmitOutput {
+ getEmitOutputStack.push(fileName);
+ if (getEmitOutputPassThrough) {
+ return originals._service.getEmitOutput.call(
+ originals._service,
+ fileName
+ );
+ }
+ if (fileName in emittedFiles) {
+ return {
+ outputFiles: [{ text: emittedFiles[fileName] }] as any,
+ emitSkipped: false
+ };
+ }
+ return { outputFiles: [], emitSkipped: false };
+ },
+ getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
+ return originals._service.getSemanticDiagnostics.call(
+ originals._service,
+ fileName
+ );
+ },
+ getSyntacticDiagnostics(fileName: string): ts.Diagnostic[] {
+ return originals._service.getSyntacticDiagnostics.call(
+ originals._service,
+ fileName
+ );
+ }
+};
+const windowMock: { define?: compiler.AmdDefine } = {};
+const mocks = {
+ _globalEval: globalEvalMock,
+ _log: logMock,
+ _os: osMock,
+ _ts: tsMock,
+ _service: serviceMock,
+ _window: windowMock
+};
+
+// Setup the mocks
+test(function compilerTestsSetup() {
+ assert("_globalEval" in compilerInstance);
+ assert("_log" in compilerInstance);
+ assert("_os" in compilerInstance);
+ assert("_ts" in compilerInstance);
+ assert("_service" in compilerInstance);
+ assert("_window" in compilerInstance);
+ Object.assign(compilerInstance, mocks);
+});
+
+test(function compilerInstance() {
+ assert(DenoCompiler != null);
+ assert(DenoCompiler.instance() != null);
+});
+
+// Testing the internal APIs
+
+test(function compilerMakeDefine() {
+ const moduleMetaData = new ModuleMetaData(
+ "/root/project/foo/bar.ts",
+ fooBarTsSource,
+ fooBarTsOutput
+ );
+ const localDefine = compilerInstance.makeDefine(moduleMetaData);
+ let factoryCalled = false;
+ localDefine(
+ ["require", "exports", "compiler"],
+ (_require, _exports, _compiler): void => {
+ factoryCalled = true;
+ assertEqual(
+ typeof _require,
+ "function",
+ "localRequire should be a function"
+ );
+ assert(_exports != null);
+ assert(
+ Object.keys(_exports).length === 0,
+ "exports should have no properties"
+ );
+ assert(compiler === _compiler, "compiler should be passed to factory");
+ }
+ );
+ assert(factoryCalled, "Factory expected to be called");
+});
+
+// TODO testMakeDefineExternalModule - testing that make define properly runs
+// external modules, this is implicitly tested though in
+// `compilerRunMultiModule`
+
+test(function compilerRun() {
+ // equal to `deno foo/bar.ts`
+ reset();
+ const result = compilerInstance.run("foo/bar.ts", "/root/project");
+ assert(result instanceof ModuleMetaData);
+ assertEqual(codeFetchStack.length, 1);
+ assertEqual(codeCacheStack.length, 1);
+ assertEqual(globalEvalStack.length, 1);
+
+ const lastGlobalEval = globalEvalStack.pop();
+ assertEqual(lastGlobalEval, fooBarTsOutput);
+ const lastCodeFetch = codeFetchStack.pop();
+ assertEqual(lastCodeFetch, {
+ moduleSpecifier: "foo/bar.ts",
+ containingFile: "/root/project"
+ });
+ const lastCodeCache = codeCacheStack.pop();
+ assertEqual(lastCodeCache, {
+ fileName: "/root/project/foo/bar.ts",
+ sourceCode: fooBarTsSource,
+ outputCode: fooBarTsOutput
+ });
+});
+
+test(function compilerRunMultiModule() {
+ // equal to `deno foo/baz.ts`
+ reset();
+ let factoryRun = false;
+ mockDeps = ["require", "exports", "compiler"];
+ mockFactory = (...deps: any[]) => {
+ const [_require, _exports, _compiler] = deps;
+ assertEqual(typeof _require, "function");
+ assertEqual(typeof _exports, "object");
+ assertEqual(_compiler, compiler);
+ factoryRun = true;
+ Object.defineProperty(_exports, "__esModule", { value: true });
+ _exports.foo = "bar";
+ // it is too complicated to test the outer factory, because the localised
+ // make define already has a reference to this factory and it can't really
+ // be easily unwound. So we will do what we can with the inner one and
+ // then just clear it...
+ mockDeps = undefined;
+ mockFactory = undefined;
+ };
+
+ const result = compilerInstance.run("foo/baz.ts", "/root/project");
+ assert(result instanceof ModuleMetaData);
+ // we have mocked that foo/bar.ts is already cached, so two fetches,
+ // but only a single cache
+ assertEqual(codeFetchStack.length, 2);
+ assertEqual(codeCacheStack.length, 1);
+ // because of the challenges with the way the module factories are generated
+ // we only get one invocation of the `globalEval` mock.
+ assertEqual(globalEvalStack.length, 1);
+ assert(factoryRun);
+});
+
+// TypeScript LanguageServiceHost APIs
+
+test(function compilerGetCompilationSettings() {
+ const result = compilerInstance.getCompilationSettings();
+ for (const key of [
+ "allowJs",
+ "module",
+ "outDir",
+ "inlineSourceMap",
+ "inlineSources",
+ "stripComments",
+ "target"
+ ]) {
+ assert(key in result, `Expected "${key}" in compiler options.`);
+ }
+});
+
+test(function compilerGetNewLine() {
+ const result = compilerInstance.getNewLine();
+ assertEqual(result, "\n", "Expected newline value of '\\n'.");
+});
+
+test(function compilerGetScriptFileNames() {
+ compilerInstance.run("foo/bar.ts", "/root/project");
+ const result = compilerInstance.getScriptFileNames();
+ assertEqual(result.length, 1, "Expected only a single filename.");
+ assertEqual(result[0], "/root/project/foo/bar.ts");
+});
+
+test(function compilerGetScriptKind() {
+ assertEqual(compilerInstance.getScriptKind("foo.ts"), ScriptKind.TS);
+ assertEqual(compilerInstance.getScriptKind("foo.d.ts"), ScriptKind.TS);
+ assertEqual(compilerInstance.getScriptKind("foo.js"), ScriptKind.JS);
+ assertEqual(compilerInstance.getScriptKind("foo.json"), ScriptKind.JSON);
+ assertEqual(compilerInstance.getScriptKind("foo.txt"), ScriptKind.JS);
+});
+
+test(function compilerGetScriptVersion() {
+ const moduleMetaData = compilerInstance.resolveModule(
+ "foo/bar.ts",
+ "/root/project"
+ );
+ assertEqual(
+ compilerInstance.getScriptVersion(moduleMetaData.fileName),
+ "1",
+ "Expected known module to have script version of 1"
+ );
+});
+
+test(function compilerGetScriptVersionUnknown() {
+ assertEqual(
+ compilerInstance.getScriptVersion("/root/project/unknown_module.ts"),
+ "",
+ "Expected unknown module to have an empty script version"
+ );
+});
+
+test(function compilerGetScriptSnapshot() {
+ const moduleMetaData = compilerInstance.resolveModule(
+ "foo/bar.ts",
+ "/root/project"
+ );
+ const result = compilerInstance.getScriptSnapshot(moduleMetaData.fileName);
+ assert(result != null, "Expected snapshot to be defined.");
+ assertEqual(result.getLength(), fooBarTsSource.length);
+ assertEqual(
+ result.getText(0, 6),
+ "import",
+ "Expected .getText() to equal 'import'"
+ );
+ assertEqual(result.getChangeRange(result), undefined);
+ assert(!("dispose" in result));
+});
+
+test(function compilerGetCurrentDirectory() {
+ assertEqual(compilerInstance.getCurrentDirectory(), "");
+});
+
+test(function compilerGetDefaultLibFileName() {
+ assertEqual(
+ compilerInstance.getDefaultLibFileName(),
+ "$asset$/lib.globals.d.ts"
+ );
+});
+
+test(function compilerUseCaseSensitiveFileNames() {
+ assertEqual(compilerInstance.useCaseSensitiveFileNames(), true);
+});
+
+test(function compilerReadFile() {
+ let doesThrow = false;
+ try {
+ compilerInstance.readFile("foobar.ts");
+ } catch (e) {
+ doesThrow = true;
+ assert(e.message.includes("Not implemented") === true);
+ }
+ assert(doesThrow);
+});
+
+test(function compilerFileExists() {
+ const moduleMetaData = compilerInstance.resolveModule(
+ "foo/bar.ts",
+ "/root/project"
+ );
+ assert(compilerInstance.fileExists(moduleMetaData.fileName));
+ assert(compilerInstance.fileExists("$asset$/compiler.d.ts"));
+ assertEqual(
+ compilerInstance.fileExists("/root/project/unknown-module.ts"),
+ false
+ );
+});
+
+test(function compilerResolveModuleNames() {
+ const results = compilerInstance.resolveModuleNames(
+ ["foo/bar.ts", "foo/baz.ts", "$asset$/lib.globals.d.ts", "deno"],
+ "/root/project"
+ );
+ assertEqual(results.length, 4);
+ const fixtures: Array<[string, boolean]> = [
+ ["/root/project/foo/bar.ts", false],
+ ["/root/project/foo/baz.ts", false],
+ ["$asset$/lib.globals.d.ts", true],
+ ["$asset$/deno.d.ts", true]
+ ];
+ for (let i = 0; i < results.length; i++) {
+ const result = results[i];
+ const [resolvedFileName, isExternalLibraryImport] = fixtures[i];
+ assertEqual(result.resolvedFileName, resolvedFileName);
+ assertEqual(result.isExternalLibraryImport, isExternalLibraryImport);
+ }
+});
+
+// Remove the mocks
+test(function compilerTestsTeardown() {
+ Object.assign(compilerInstance, originals);
+});
diff --git a/js/globals.ts b/js/globals.ts
index ebfd3d265..beecbf58d 100644
--- a/js/globals.ts
+++ b/js/globals.ts
@@ -1,6 +1,7 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import { Console } from "./console";
+import { exit } from "./os";
import { RawSourceMap } from "./types";
import * as timers from "./timers";
import { TextEncoder, TextDecoder } from "./text_encoding";
@@ -9,6 +10,14 @@ import * as fetch_ from "./fetch";
declare global {
interface Window {
console: Console;
+ define: Readonly<unknown>;
+ onerror?: (
+ message: string,
+ source: string,
+ lineno: number,
+ colno: number,
+ error: Error
+ ) => void;
}
const clearTimeout: typeof timers.clearTimer;
@@ -58,6 +67,22 @@ window.clearTimeout = timers.clearTimer;
window.clearInterval = timers.clearTimer;
window.console = new Console(libdeno.print);
+// Uncaught exceptions are sent to window.onerror by the privileged binding.
+window.onerror = (
+ message: string,
+ source: string,
+ lineno: number,
+ colno: number,
+ error: Error
+) => {
+ // TODO Currently there is a bug in v8_source_maps.ts that causes a
+ // segfault if it is used within window.onerror. To workaround we
+ // uninstall the Error.prepareStackTrace handler. Users will get unmapped
+ // stack traces on uncaught exceptions until this issue is fixed.
+ //Error.prepareStackTrace = null;
+ console.log(error.stack);
+ exit(1);
+};
window.TextEncoder = TextEncoder;
window.TextDecoder = TextDecoder;
diff --git a/js/main.ts b/js/main.ts
index d035f9ba6..740003049 100644
--- a/js/main.ts
+++ b/js/main.ts
@@ -1,10 +1,9 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import { flatbuffers } from "flatbuffers";
import { deno as fbs } from "gen/msg_generated";
-import { assert, log, assignCmdId } from "./util";
-import * as util from "./util";
+import { assert, assignCmdId, log, setLogDebug } from "./util";
import * as os from "./os";
-import * as runtime from "./runtime";
+import { DenoCompiler } from "./compiler";
import { libdeno } from "./globals";
import * as timers from "./timers";
import { onFetchRes } from "./fetch";
@@ -47,7 +46,7 @@ function onMessage(ui8: Uint8Array) {
/* tslint:disable-next-line:no-default-export */
export default function denoMain() {
libdeno.recv(onMessage);
- runtime.setup();
+ const compiler = DenoCompiler.instance();
// First we send an empty "Start" message to let the privlaged side know we
// are ready. The response should be a "StartRes" message containing the CLI
@@ -69,7 +68,7 @@ export default function denoMain() {
const startResMsg = new fbs.StartRes();
assert(base.msg(startResMsg) != null);
- util.setLogDebug(startResMsg.debugFlag());
+ setLogDebug(startResMsg.debugFlag());
const cwd = startResMsg.cwd();
log("cwd", cwd);
@@ -86,8 +85,5 @@ export default function denoMain() {
os.exit(1);
}
- const mod = runtime.resolveModule(inputFn, `${cwd}/`);
- assert(mod != null);
- // TypeScript does not track assert, therefore not null assertion
- mod!.compileAndRun();
+ compiler.run(inputFn, `${cwd}/`);
}
diff --git a/js/runtime.ts b/js/runtime.ts
deleted file mode 100644
index 93649e11c..000000000
--- a/js/runtime.ts
+++ /dev/null
@@ -1,376 +0,0 @@
-// Copyright 2018 the Deno authors. All rights reserved. MIT license.
-// Glossary
-// outputCode = generated javascript code
-// sourceCode = typescript code (or input javascript code)
-// moduleName = a resolved module name
-// fileName = an unresolved raw fileName.
-// for http modules , its the path to the locally downloaded
-// version.
-
-import * as ts from "typescript";
-import * as util from "./util";
-import { log } from "./util";
-import { assetSourceCode } from "./assets";
-import * as os from "./os";
-import * as sourceMaps from "./v8_source_maps";
-import { libdeno, window, globalEval } from "./globals";
-import * as deno from "./deno";
-import { RawSourceMap } from "./types";
-
-const EOL = "\n";
-const ASSETS = "/$asset$/";
-
-// tslint:disable-next-line:no-any
-export type AmdFactory = (...args: any[]) => undefined | object;
-export type AmdDefine = (deps: string[], factory: AmdFactory) => void;
-
-// Uncaught exceptions are sent to window.onerror by the privlaged binding.
-window.onerror = (
- message: string,
- source: string,
- lineno: number,
- colno: number,
- error: Error
-) => {
- // TODO Currently there is a bug in v8_source_maps.ts that causes a segfault
- // if it is used within window.onerror. To workaround we uninstall the
- // Error.prepareStackTrace handler. Users will get unmapped stack traces on
- // uncaught exceptions until this issue is fixed.
- //Error.prepareStackTrace = null;
- console.log(error.stack);
- os.exit(1);
-};
-
-export function setup(): void {
- sourceMaps.install({
- installPrepareStackTrace: true,
- getGeneratedContents: (filename: string): string | RawSourceMap => {
- util.log("getGeneratedContents", filename);
- if (filename === "gen/bundle/main.js") {
- util.assert(libdeno.mainSource.length > 0);
- return libdeno.mainSource;
- } else if (filename === "main.js.map") {
- return libdeno.mainSourceMap;
- } else if (filename === "deno_main.js") {
- return "";
- } else {
- const mod = FileModule.load(filename);
- if (!mod) {
- util.log("getGeneratedContents cannot find", filename);
- return "";
- }
- return mod.outputCode;
- }
- }
- });
-}
-
-// This class represents a module. We call it FileModule to make it explicit
-// that each module represents a single file.
-// Access to FileModule instances should only be done thru the static method
-// FileModule.load(). FileModules are NOT executed upon first load, only when
-// compileAndRun is called.
-export class FileModule {
- scriptVersion = "";
- readonly exports = {};
-
- private static readonly map = new Map<string, FileModule>();
- constructor(
- readonly fileName: string,
- readonly sourceCode = "",
- public outputCode = ""
- ) {
- util.assert(
- !FileModule.map.has(fileName),
- `FileModule.map already has ${fileName}`
- );
- FileModule.map.set(fileName, this);
- if (outputCode !== "") {
- this.scriptVersion = "1";
- }
- }
-
- compileAndRun(): void {
- util.log("compileAndRun", this.sourceCode);
- if (!this.outputCode) {
- // If there is no cached outputCode, then compile the code.
- util.assert(
- this.sourceCode != null && this.sourceCode.length > 0,
- `Have no source code from ${this.fileName}`
- );
- const compiler = Compiler.instance();
- this.outputCode = compiler.compile(this.fileName);
- os.codeCache(this.fileName, this.sourceCode, this.outputCode);
- }
- execute(this.fileName, this.outputCode);
- }
-
- static load(fileName: string): FileModule | undefined {
- return this.map.get(fileName);
- }
-
- static getScriptsWithSourceCode(): string[] {
- const out = [];
- for (const fn of this.map.keys()) {
- const m = this.map.get(fn);
- if (m && m.sourceCode) {
- out.push(fn);
- }
- }
- return out;
- }
-}
-
-export function makeDefine(fileName: string): AmdDefine {
- const localDefine = (deps: string[], factory: AmdFactory): void => {
- const localRequire = (x: string) => {
- log("localRequire", x);
- };
- const currentModule = FileModule.load(fileName);
- util.assert(currentModule != null);
- const localExports = currentModule!.exports;
- log("localDefine", fileName, deps, localExports);
- const args = deps.map(dep => {
- if (dep === "require") {
- return localRequire;
- } else if (dep === "exports") {
- return localExports;
- } else if (dep === "typescript") {
- return ts;
- } else if (dep === "deno") {
- return deno;
- } else {
- const resolved = resolveModuleName(dep, fileName);
- util.assert(resolved != null);
- const depModule = FileModule.load(resolved!);
- if (depModule) {
- depModule.compileAndRun();
- return depModule.exports;
- }
- return undefined;
- }
- });
- factory(...args);
- };
- return localDefine;
-}
-
-export function resolveModule(
- moduleSpecifier: string,
- containingFile: string
-): null | FileModule {
- util.log("resolveModule", { moduleSpecifier, containingFile });
- util.assert(moduleSpecifier != null && moduleSpecifier.length > 0);
- let filename: string | null;
- let sourceCode: string | null;
- let outputCode: string | null;
- if (moduleSpecifier.startsWith(ASSETS) || containingFile.startsWith(ASSETS)) {
- // Assets are compiled into the runtime javascript bundle.
- // we _know_ `.pop()` will return a string, but TypeScript doesn't so
- // not null assertion
- const moduleId = moduleSpecifier.split("/").pop()!;
- const assetName = moduleId.includes(".") ? moduleId : `${moduleId}.d.ts`;
- util.assert(assetName in assetSourceCode, `No such asset "${assetName}"`);
- sourceCode = assetSourceCode[assetName];
- filename = ASSETS + assetName;
- } else {
- // We query Rust with a CodeFetch message. It will load the sourceCode, and
- // if there is any outputCode cached, will return that as well.
- const fetchResponse = os.codeFetch(moduleSpecifier, containingFile);
- filename = fetchResponse.filename;
- sourceCode = fetchResponse.sourceCode;
- outputCode = fetchResponse.outputCode;
- }
- if (sourceCode == null || sourceCode.length === 0 || filename == null) {
- return null;
- }
- util.log("resolveModule sourceCode length ", sourceCode.length);
- const m = FileModule.load(filename);
- if (m != null) {
- return m;
- } else {
- // null and undefined are incompatible in strict mode, but outputCode being
- // null here has no runtime behavior impact, therefore not null assertion
- return new FileModule(filename, sourceCode, outputCode!);
- }
-}
-
-function resolveModuleName(
- moduleSpecifier: string,
- containingFile: string
-): string | undefined {
- const mod = resolveModule(moduleSpecifier, containingFile);
- if (mod) {
- return mod.fileName;
- } else {
- return undefined;
- }
-}
-
-function execute(fileName: string, outputCode: string): void {
- util.assert(outputCode != null && outputCode.length > 0);
- window["define"] = makeDefine(fileName);
- outputCode += `\n//# sourceURL=${fileName}`;
- globalEval(outputCode);
- window["define"] = null;
-}
-
-// This is a singleton class. Use Compiler.instance() to access.
-class Compiler {
- options: ts.CompilerOptions = {
- allowJs: true,
- module: ts.ModuleKind.AMD,
- outDir: "$deno$",
- inlineSourceMap: true,
- inlineSources: true,
- target: ts.ScriptTarget.ESNext
- };
- /*
- allowJs: true,
- module: ts.ModuleKind.AMD,
- noEmit: false,
- outDir: '$deno$',
- */
- private service: ts.LanguageService;
-
- private constructor() {
- const host = new TypeScriptHost(this.options);
- this.service = ts.createLanguageService(host);
- }
-
- private static _instance: Compiler;
- static instance(): Compiler {
- return this._instance || (this._instance = new this());
- }
-
- compile(fileName: string): string {
- const output = this.service.getEmitOutput(fileName);
-
- // Get the relevant diagnostics - this is 3x faster than
- // `getPreEmitDiagnostics`.
- const diagnostics = this.service
- .getCompilerOptionsDiagnostics()
- .concat(this.service.getSyntacticDiagnostics(fileName))
- .concat(this.service.getSemanticDiagnostics(fileName));
- if (diagnostics.length > 0) {
- const errMsg = ts.formatDiagnosticsWithColorAndContext(
- diagnostics,
- formatDiagnosticsHost
- );
- console.log(errMsg);
- os.exit(1);
- }
-
- util.assert(!output.emitSkipped);
-
- const outputCode = output.outputFiles[0].text;
- // let sourceMapCode = output.outputFiles[0].text;
- return outputCode;
- }
-}
-
-// Create the compiler host for type checking.
-class TypeScriptHost implements ts.LanguageServiceHost {
- constructor(readonly options: ts.CompilerOptions) {}
-
- getScriptFileNames(): string[] {
- const keys = FileModule.getScriptsWithSourceCode();
- util.log("getScriptFileNames", keys);
- return keys;
- }
-
- getScriptVersion(fileName: string): string {
- util.log("getScriptVersion", fileName);
- const m = FileModule.load(fileName);
- return (m && m.scriptVersion) || "";
- }
-
- getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined {
- util.log("getScriptSnapshot", fileName);
- const m = resolveModule(fileName, ".");
- if (m == null) {
- util.log("getScriptSnapshot", fileName, "NOT FOUND");
- return undefined;
- }
- //const m = resolveModule(fileName, ".");
- util.assert(m.sourceCode.length > 0);
- return ts.ScriptSnapshot.fromString(m.sourceCode);
- }
-
- fileExists(fileName: string): boolean {
- const m = resolveModule(fileName, ".");
- const exists = m != null;
- util.log("fileExist", fileName, exists);
- return exists;
- }
-
- readFile(path: string, encoding?: string): string | undefined {
- util.log("readFile", path);
- return util.notImplemented();
- }
-
- getNewLine() {
- return EOL;
- }
-
- getCurrentDirectory() {
- util.log("getCurrentDirectory");
- return ".";
- }
-
- getCompilationSettings() {
- util.log("getCompilationSettings");
- return this.options;
- }
-
- getDefaultLibFileName(options: ts.CompilerOptions): string {
- const fn = "lib.globals.d.ts"; // ts.getDefaultLibFileName(options);
- util.log("getDefaultLibFileName", fn);
- const m = resolveModule(fn, ASSETS);
- util.assert(m != null);
- // TypeScript cannot track assertions, therefore not null assertion
- return m!.fileName;
- }
-
- resolveModuleNames(
- moduleNames: string[],
- containingFile: string
- ): ts.ResolvedModule[] {
- //util.log("resolveModuleNames", { moduleNames, reusedNames });
- return moduleNames.map(name => {
- let resolvedFileName;
- if (name === "deno") {
- resolvedFileName = resolveModuleName("deno.d.ts", ASSETS);
- } else if (name === "typescript") {
- resolvedFileName = resolveModuleName("typescript.d.ts", ASSETS);
- } else {
- resolvedFileName = resolveModuleName(name, containingFile);
- }
- // According to the interface we shouldn't return `undefined` but if we
- // fail to return the same length of modules to those we cannot resolve
- // then TypeScript fails on an assertion that the lengths can't be
- // different, so we have to return an "empty" resolved module
- // TODO: all this does is push the problem downstream, and TypeScript
- // will complain it can't identify the type of the file and throw
- // a runtime exception, so we need to handle missing modules better
- resolvedFileName = resolvedFileName || "";
- // This flags to the compiler to not go looking to transpile functional
- // code, anything that is in `/$asset$/` is just library code
- const isExternalLibraryImport = resolvedFileName.startsWith(ASSETS);
- // TODO: we should be returning a ts.ResolveModuleFull
- return { resolvedFileName, isExternalLibraryImport };
- });
- }
-}
-
-const formatDiagnosticsHost: ts.FormatDiagnosticsHost = {
- getCurrentDirectory(): string {
- return ".";
- },
- getCanonicalFileName(fileName: string): string {
- return fileName;
- },
- getNewLine(): string {
- return EOL;
- }
-};
diff --git a/js/tsconfig.generated.json b/js/tsconfig.generated.json
index 4d76dd98f..d3cacd73e 100644
--- a/js/tsconfig.generated.json
+++ b/js/tsconfig.generated.json
@@ -11,6 +11,7 @@
},
"files": [
"../node_modules/typescript/lib/lib.esnext.d.ts",
+ "./compiler.ts",
"./deno.ts",
"./globals.ts"
]
diff --git a/js/unit_tests.ts b/js/unit_tests.ts
index ef65519f6..f203444fa 100644
--- a/js/unit_tests.ts
+++ b/js/unit_tests.ts
@@ -6,6 +6,8 @@
import { test, assert, assertEqual } from "./testing/testing.ts";
import { readFileSync } from "deno";
+import "./compiler_test.ts";
+
test(async function tests_test() {
assert(true);
});
diff --git a/tests/013_dynamic_import.ts b/tests/013_dynamic_import.ts
new file mode 100644
index 000000000..0812623f6
--- /dev/null
+++ b/tests/013_dynamic_import.ts
@@ -0,0 +1,17 @@
+(async () => {
+ const {
+ returnsHi,
+ returnsFoo2,
+ printHello3
+ } = await import("./subdir/mod1.ts");
+
+ printHello3();
+
+ if (returnsHi() !== "Hi") {
+ throw Error("Unexpected");
+ }
+
+ if (returnsFoo2() !== "Foo") {
+ throw Error("Unexpected");
+ }
+})();
diff --git a/tests/013_dynamic_import.ts.out b/tests/013_dynamic_import.ts.out
new file mode 100644
index 000000000..e965047ad
--- /dev/null
+++ b/tests/013_dynamic_import.ts.out
@@ -0,0 +1 @@
+Hello
diff --git a/tests/async_error.ts.out b/tests/async_error.ts.out
index c98c79bfc..f282f6c3e 100644
--- a/tests/async_error.ts.out
+++ b/tests/async_error.ts.out
@@ -3,8 +3,7 @@ before error
Error: error
at foo ([WILDCARD]tests/async_error.ts:4:9)
at eval ([WILDCARD]tests/async_error.ts:7:1)
- at eval (<anonymous>)
- at execute (deno/js/runtime.ts:[WILDCARD])
- at FileModule.compileAndRun (deno/js/runtime.ts:[WILDCARD])
+ at DenoCompiler.eval [as _globalEval] (<anonymous>)
+ at DenoCompiler.run (deno/js/compiler.ts:[WILDCARD])
at denoMain (deno/js/main.ts:[WILDCARD])
at deno_main.js:1:1
diff --git a/tests/error_001.ts.out b/tests/error_001.ts.out
index 237af9d57..04d9abb96 100644
--- a/tests/error_001.ts.out
+++ b/tests/error_001.ts.out
@@ -2,8 +2,7 @@ Error: bad
at foo (file://[WILDCARD]tests/error_001.ts:2:9)
at bar (file://[WILDCARD]tests/error_001.ts:6:3)
at eval (file://[WILDCARD]tests/error_001.ts:9:1)
- at eval (<anonymous>)
- at execute (deno/js/runtime.ts:[WILDCARD])
- at FileModule.compileAndRun (deno/js/runtime.ts:[WILDCARD])
+ at DenoCompiler.eval [as _globalEval] (<anonymous>)
+ at DenoCompiler.run (deno/js/compiler.ts:[WILDCARD])
at denoMain (deno/js/main.ts:[WILDCARD])
at deno_main.js:1:1
diff --git a/tests/error_002.ts.out b/tests/error_002.ts.out
index f245ab7c0..20eb0c842 100644
--- a/tests/error_002.ts.out
+++ b/tests/error_002.ts.out
@@ -2,10 +2,9 @@ Error: exception from mod1
at Object.throwsError (file://[WILDCARD]tests/subdir/mod1.ts:16:9)
at foo (file://[WILDCARD]tests/error_002.ts:4:3)
at eval (file://[WILDCARD]tests/error_002.ts:7:1)
- at localDefine (deno/js/runtime.ts:[WILDCARD])
+ at localDefine (deno/js/compiler.ts:[WILDCARD])
at eval ([WILDCARD]tests/error_002.ts, <anonymous>)
- at eval (<anonymous>)
- at execute (deno/js/runtime.ts:[WILDCARD])
- at FileModule.compileAndRun (deno/js/runtime.ts:[WILDCARD])
+ at DenoCompiler.eval [as _globalEval] (<anonymous>)
+ at DenoCompiler.run (deno/js/compiler.ts:[WILDCARD])
at denoMain (deno/js/main.ts:[WILDCARD])
at deno_main.js:1:1
diff --git a/tests/error_003_typescript.ts b/tests/error_003_typescript.ts
new file mode 100644
index 000000000..ebd9fcbe6
--- /dev/null
+++ b/tests/error_003_typescript.ts
@@ -0,0 +1,2 @@
+// console.log intentionally misspelled to trigger TypeScript error
+consol.log("hello world!");
diff --git a/tests/error_003_typescript.ts.out b/tests/error_003_typescript.ts.out
new file mode 100644
index 000000000..f04be363b
--- /dev/null
+++ b/tests/error_003_typescript.ts.out
@@ -0,0 +1,10 @@
+[WILDCARD]tests/error_003_typescript.tsILDCARD] - error TS2552: Cannot find name 'consol'. Did you mean 'console'?
+
+[WILDCARD][0m consol.log("hello world!");
+  ~~~~~~
+
+ $asset$/globals.d.tsILDCARD]
+ [WILDCARD][0m const console: Console;
+    ~~~~~~~
+ 'console' is declared here.
+
diff --git a/tests/error_004_missing_module.ts b/tests/error_004_missing_module.ts
new file mode 100644
index 000000000..48623320b
--- /dev/null
+++ b/tests/error_004_missing_module.ts
@@ -0,0 +1 @@
+import * as badModule from "bad-module.ts";
diff --git a/tests/error_004_missing_module.ts.out b/tests/error_004_missing_module.ts.out
new file mode 100644
index 000000000..dda14d25e
--- /dev/null
+++ b/tests/error_004_missing_module.ts.out
@@ -0,0 +1,12 @@
+Error: Cannot resolve module "bad-module.ts" from "[WILDCARD]error_004_missing_module.ts".
+ os.codeFetch message: [WILDCARD] (os error 2)
+ at throwResolutionError (deno/js/compiler.ts:[WILDCARD])
+ at DenoCompiler.resolveModule (deno/js/compiler.ts:[WILDCARD])
+ at DenoCompiler.resolveModuleName (deno/js/compiler.ts:[WILDCARD])
+ at moduleNames.map.name (deno/js/compiler.ts:[WILDCARD])
+ at Array.map (<anonymous>)
+ at DenoCompiler.resolveModuleNames (deno/js/compiler.ts:[WILDCARD])
+ at Object.compilerHost.resolveModuleNames (deno/third_party/node_modules/typescript/lib/typescript.js:[WILDCARD])
+ at resolveModuleNamesWorker (deno/third_party/node_modules/typescript/lib/typescript.js:[WILDCARD])
+ at resolveModuleNamesReusingOldState (deno/third_party/node_modules/typescript/lib/typescript.js:[WILDCARD])
+ at processImportedModules (deno/third_party/node_modules/typescript/lib/typescript.js:[WILDCARD])