// Glossary // outputCode = generated javascript code // sourceCode = typescript code (or input javascript code) // fileName = an unresolved raw fileName. // moduleName = a resolved module name import * as ts from "typescript"; import * as path from "path"; import * as util from "./util"; import { log } from "./util"; import * as os from "./os"; // 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 executed upon first load. export class FileModule { scriptVersion: string = undefined; sourceCode: string; outputCode: string; readonly exports = {}; private static readonly map = new Map(); private constructor(readonly fileName: string) { FileModule.map.set(fileName, this); assertValidFileName(this.fileName); // Load typescript code (sourceCode) and maybe load compiled javascript // (outputCode) from cache. If cache is empty, outputCode will be null. const { sourceCode, outputCode } = os.sourceCodeFetch(this.fileName); this.sourceCode = sourceCode; this.outputCode = outputCode; this.scriptVersion = "1"; } compileAndRun() { if (!this.outputCode) { // If there is no cached outputCode, the compile the code. util.assert(this.sourceCode && this.sourceCode.length > 0); const compiler = Compiler.instance(); this.outputCode = compiler.compile(this.fileName); os.sourceCodeCache(this.fileName, this.sourceCode, this.outputCode); } util.log("compileAndRun", this.sourceCode); execute(this.fileName, this.outputCode); } static load(fileName: string): FileModule { assertValidFileName(fileName); let m = this.map.get(fileName); if (m == null) { m = new this(fileName); util.assert(this.map.has(fileName)); } return m; } static getScriptsWithSourceCode(): string[] { const out = []; for (const fn of this.map.keys()) { const m = this.map.get(fn); if (m.sourceCode) { out.push(fn); } } return out; } } function assertValidFileName(fileName: string): void { if (fileName !== "lib.d.ts") { util.assert(fileName[0] === "/", `fileName must be absolute: ${fileName}`); } } // tslint:disable-next-line:no-any type AmdFactory = (...args: any[]) => undefined | object; type AmdDefine = (deps: string[], factory: AmdFactory) => void; 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); 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 { dep = resolveModuleName(dep, fileName); const depModule = FileModule.load(dep); depModule.compileAndRun(); return depModule.exports; } }); factory(...args); }; return localDefine; } function resolveModuleName(fileName: string, contextFileName: string): string { return path.resolve(path.dirname(contextFileName), fileName); } function execute(fileName: string, outputCode: string): void { util.assert(outputCode && outputCode.length > 0); util._global["define"] = makeDefine(fileName); util.globalEval(outputCode); util._global["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$" }; /* allowJs: true, inlineSourceMap: true, inlineSources: 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) { throw Error("diagnotics"); } util.log("compile output", output); 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.scriptVersion; } getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { util.log("getScriptSnapshot", fileName); const m = FileModule.load(fileName); if (m.sourceCode) { return ts.ScriptSnapshot.fromString(m.sourceCode); } else { return undefined; } } fileExists(fileName: string): boolean { throw Error("not implemented"); } readFile(path: string, encoding?: string): string | undefined { util.log("readFile", path); throw Error("not implemented"); } getNewLine() { const EOL = "\n"; return EOL; } getCurrentDirectory() { util.log("getCurrentDirectory"); return "."; } getCompilationSettings() { util.log("getCompilationSettings"); return this.options; } getDefaultLibFileName(options: ts.CompilerOptions): string { util.log("getDefaultLibFileName"); return ts.getDefaultLibFileName(options); } resolveModuleNames( moduleNames: string[], containingFile: string, reusedNames?: string[] ): Array { util.log("resolveModuleNames", { moduleNames, reusedNames }); return moduleNames.map((name: string) => { if ( name.startsWith("/") || name.startsWith("http://") || name.startsWith("https://") ) { throw Error("Non-relative imports not yet supported."); } else { // Relative import. const containingDir = path.dirname(containingFile); const resolvedFileName = path.join(containingDir, name); util.log("relative import", { containingFile, name, resolvedFileName }); const isExternalLibraryImport = false; return { resolvedFileName, isExternalLibraryImport }; } }); } }