diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2018-08-27 21:22:57 -0700 |
---|---|---|
committer | Ryan Dahl <ry@tinyclouds.org> | 2018-08-28 08:44:34 -0400 |
commit | 54aefa2886ea7c6130b236b060cbb04d9efc2733 (patch) | |
tree | f2e20395bda95d93f62209cbc922eb4b613a2ff5 /js/compiler.ts | |
parent | 9960b1d22b474d87d03855e2aefd8c552ce02e58 (diff) |
Two-pass module evaluation.
Plus changes to tests to accomodate.
Diffstat (limited to 'js/compiler.ts')
-rw-r--r-- | js/compiler.ts | 244 |
1 files changed, 185 insertions, 59 deletions
diff --git a/js/compiler.ts b/js/compiler.ts index 46f8c8121..5e5850007 100644 --- a/js/compiler.ts +++ b/js/compiler.ts @@ -18,7 +18,12 @@ 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; +export type AmdDefine = (deps: ModuleSpecifier[], factory: AmdFactory) => void; +type AMDRequire = ( + deps: ModuleSpecifier[], + 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 @@ -60,7 +65,10 @@ export interface Ts { * the module, not the actual module instance. */ export class ModuleMetaData implements ts.IScriptSnapshot { + public deps?: ModuleFileName[]; public readonly exports = {}; + public factory?: AmdFactory; + public hasRun = false; public scriptVersion = ""; constructor( @@ -155,6 +163,9 @@ export class DenoCompiler implements ts.LanguageServiceHost { // A reference to the `./os.ts` module, so it can be monkey patched during // testing private _os: Os = os; + // Contains a queue of modules that have been resolved, but not yet + // run + private _runQueue: ModuleMetaData[] = []; // Used to contain the script file we are currently running private _scriptFileNames: string[] = []; // A reference to the TypeScript LanguageService instance so it can be @@ -168,6 +179,78 @@ export class DenoCompiler implements ts.LanguageServiceHost { private _window = window; /** + * Drain the run queue, retrieving the arguments for the module + * factory and calling the module's factory. + */ + private _drainRunQueue(): void { + this._log( + "compiler._drainRunQueue", + this._runQueue.map(metaData => metaData.fileName) + ); + let moduleMetaData: ModuleMetaData | undefined; + while ((moduleMetaData = this._runQueue.shift())) { + assert( + moduleMetaData.factory != null, + "Cannot run module without factory." + ); + assert(moduleMetaData.hasRun === false, "Module has already been run."); + // asserts not tracked by TypeScripts, so using not null operator + moduleMetaData.factory!(...this._getFactoryArguments(moduleMetaData)); + moduleMetaData.hasRun = true; + } + } + + /** + * Get the dependencies for a given module, but don't run the module, + * just add the module factory to the run queue. + */ + private _gatherDependencies(moduleMetaData: ModuleMetaData): void { + this._log("compiler._resolveDependencies", moduleMetaData.fileName); + + // if the module has already run, we can short circuit. + // it is intentional though that if we have already resolved dependencies, + // we won't short circuit, as something may have changed, or we might have + // only collected the dependencies to be able to able to obtain the graph of + // dependencies + if (moduleMetaData.hasRun) { + return; + } + + this._window.define = this.makeDefine(moduleMetaData); + this._globalEval(this.compile(moduleMetaData)); + this._window.define = undefined; + } + + /** + * Retrieve the arguments to pass a module's factory function. + */ + // tslint:disable-next-line:no-any + private _getFactoryArguments(moduleMetaData: ModuleMetaData): any[] { + if (!moduleMetaData.deps) { + throw new Error("Cannot get arguments until dependencies resolved."); + } + return moduleMetaData.deps.map(dep => { + if (dep === "require") { + return this._makeLocalRequire(moduleMetaData); + } + if (dep === "exports") { + return moduleMetaData.exports; + } + if (dep in DenoCompiler._builtins) { + return DenoCompiler._builtins[dep]; + } + const dependencyMetaData = this._getModuleMetaData(dep); + assert(dependencyMetaData != null, `Missing dependency "${dep}".`); + assert( + dependencyMetaData!.hasRun === true, + `Module "${dep}" was not run.` + ); + // TypeScript does not track assert, therefore using not null operator + return dependencyMetaData!.exports; + }); + } + + /** * 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` @@ -187,6 +270,35 @@ export class DenoCompiler implements ts.LanguageServiceHost { } /** + * Returns a require that specifically handles the resolution of a transpiled + * emit of a dynamic ES `import()` from TypeScript. + */ + private _makeLocalRequire(moduleMetaData: ModuleMetaData): AMDRequire { + const localRequire = ( + deps: ModuleSpecifier[], + callback: AmdCallback, + errback: AmdErrback + ): void => { + log("localRequire", deps); + assert( + deps.length === 1, + "Local require requires exactly one dependency." + ); + const [moduleSpecifier] = deps; + try { + const requiredMetaData = this.run( + moduleSpecifier, + moduleMetaData.fileName + ); + callback(requiredMetaData.exports); + } catch (e) { + errback(e); + } + }; + return localRequire; + } + + /** * 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 @@ -232,7 +344,12 @@ export class DenoCompiler implements ts.LanguageServiceHost { /** * Retrieve the output of the TypeScript compiler for a given `fileName`. */ - compile(fileName: ModuleFileName): OutputCode { + compile(moduleMetaData: ModuleMetaData): OutputCode { + this._log("compiler.compile", moduleMetaData.fileName); + if (moduleMetaData.outputCode) { + return moduleMetaData.outputCode; + } + const { fileName, sourceCode } = moduleMetaData; const service = this._service; const output = service.getEmitOutput(fileName); @@ -263,53 +380,72 @@ export class DenoCompiler implements ts.LanguageServiceHost { ); const [outputFile] = output.outputFiles; - return outputFile.text; + const outputCode = (moduleMetaData.outputCode = `${ + outputFile.text + }\n//# sourceURL=${fileName}`); + moduleMetaData.scriptVersion = "1"; + this._os.codeCache(fileName, sourceCode, outputCode); + return moduleMetaData.outputCode; + } + + /** + * For a given module specifier and containing file, return a list of absolute + * identifiers for dependent modules that are required by this module. + */ + getModuleDependencies( + moduleSpecifier: ModuleSpecifier, + containingFile: ContainingFile + ): ModuleFileName[] { + assert( + this._runQueue.length === 0, + "Cannot get dependencies with modules queued to be run." + ); + const moduleMetaData = this.resolveModule(moduleSpecifier, containingFile); + assert( + !moduleMetaData.hasRun, + "Cannot get dependencies for a module that has already been run." + ); + this._gatherDependencies(moduleMetaData); + const dependencies = this._runQueue.map( + moduleMetaData => moduleMetaData.fileName + ); + // empty the run queue, to free up references to factories we have collected + // and to ensure that if there is a further invocation of `.run()` the + // factories don't get called + this._runQueue = []; + return dependencies; } /** * 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 { _log: log } = this; - const localExports = moduleMetaData.exports; - - // tslint:disable-next-line:no-any - const resolveDependencies = (deps: string[]): any[] => { - return 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; - } - }); - }; - - // this is a function because we need hoisting - function localRequire( - deps: string[], - callback: AmdCallback, - errback: AmdErrback - ): void { - log("localRequire", deps); - try { - const args = resolveDependencies(deps); - callback(...args); - } catch (e) { - errback(e); + // TODO should this really be part of the public API of the compiler? + const localDefine: AmdDefine = ( + deps: ModuleSpecifier[], + factory: AmdFactory + ): void => { + this._log("compiler.localDefine", moduleMetaData.fileName); + moduleMetaData.factory = factory; + // we will recursively resolve the dependencies for any modules + moduleMetaData.deps = deps.map(dep => { + if ( + dep === "require" || + dep === "exports" || + dep in DenoCompiler._builtins + ) { + return dep; } + const dependencyMetaData = this.resolveModule( + dep, + moduleMetaData.fileName + ); + this._gatherDependencies(dependencyMetaData); + return dependencyMetaData.fileName; + }); + if (!this._runQueue.includes(moduleMetaData)) { + this._runQueue.push(moduleMetaData); } - - this._log("localDefine", moduleMetaData.fileName, deps, localExports); - const args = resolveDependencies(deps); - factory(...args); }; return localDefine; } @@ -404,32 +540,22 @@ export class DenoCompiler implements ts.LanguageServiceHost { 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`. + * Load and run a module and all of its dependencies based on a module + * specifier and a containing file */ run( moduleSpecifier: ModuleSpecifier, containingFile: ContainingFile ): ModuleMetaData { - this._log("run", { moduleSpecifier, containingFile }); + this._log("compiler.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._scriptFileNames = [moduleMetaData.fileName]; + if (!moduleMetaData.deps) { + this._gatherDependencies(moduleMetaData); } - this._window.define = this.makeDefine(moduleMetaData); - this._globalEval(moduleMetaData.outputCode); - this._window.define = undefined; - return moduleMetaData!; + this._drainRunQueue(); + return moduleMetaData; } /** |