diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/assets.ts | 2 | ||||
-rw-r--r-- | js/compiler.ts | 595 | ||||
-rw-r--r-- | js/compiler_test.ts | 471 | ||||
-rw-r--r-- | js/globals.ts | 25 | ||||
-rw-r--r-- | js/main.ts | 14 | ||||
-rw-r--r-- | js/runtime.ts | 376 | ||||
-rw-r--r-- | js/tsconfig.generated.json | 1 | ||||
-rw-r--r-- | js/unit_tests.ts | 2 |
8 files changed, 1101 insertions, 385 deletions
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); }); |