1namespace evaluator { 2 declare let Symbol: SymbolConstructor; 3 4 const sourceFile = vpath.combine(vfs.srcFolder, "source.ts"); 5 const sourceFileJs = vpath.combine(vfs.srcFolder, "source.js"); 6 7 // Define a custom "Symbol" constructor to attach missing built-in symbols without 8 // modifying the global "Symbol" constructor 9 const FakeSymbol: SymbolConstructor = ((description?: string) => Symbol(description)) as any; 10 (FakeSymbol as any).prototype = Symbol.prototype; 11 for (const key of Object.getOwnPropertyNames(Symbol)) { 12 Object.defineProperty(FakeSymbol, key, Object.getOwnPropertyDescriptor(Symbol, key)!); 13 } 14 15 // Add "asyncIterator" if missing 16 if (!ts.hasProperty(FakeSymbol, "asyncIterator")) Object.defineProperty(FakeSymbol, "asyncIterator", { value: Symbol.for("Symbol.asyncIterator"), configurable: true }); 17 18 export function evaluateTypeScript(source: string | { files: vfs.FileSet, rootFiles: string[], main: string }, options?: ts.CompilerOptions, globals?: Record<string, any>) { 19 if (typeof source === "string") source = { files: { [sourceFile]: source }, rootFiles: [sourceFile], main: sourceFile }; 20 const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { files: source.files }); 21 const compilerOptions: ts.CompilerOptions = { 22 target: ts.ScriptTarget.ES5, 23 module: ts.ModuleKind.CommonJS, 24 lib: ["lib.esnext.d.ts", "lib.dom.d.ts"], 25 ...options 26 }; 27 const host = new fakes.CompilerHost(fs, compilerOptions); 28 const result = compiler.compileFiles(host, source.rootFiles, compilerOptions); 29 if (ts.some(result.diagnostics)) { 30 assert.ok(/*value*/ false, "Syntax error in evaluation source text:\n" + ts.formatDiagnostics(result.diagnostics, { 31 getCanonicalFileName: file => file, 32 getCurrentDirectory: () => "", 33 getNewLine: () => "\n" 34 })); 35 } 36 37 const output = result.getOutput(source.main, "js")!; 38 assert.isDefined(output); 39 globals = { Symbol: FakeSymbol, ...globals }; 40 const loader = getLoader(compilerOptions, fs, globals); 41 return loader.import(output.file); 42 } 43 44 export function evaluateJavaScript(sourceText: string, globals?: Record<string, any>, sourceFile = sourceFileJs) { 45 globals = { Symbol: FakeSymbol, ...globals }; 46 const fs = new vfs.FileSystem(/*ignoreCase*/ false, { files: { [sourceFile]: sourceText } }); 47 return new CommonJsLoader(fs, globals).import(sourceFile); 48 } 49 50 function getLoader(compilerOptions: ts.CompilerOptions, fs: vfs.FileSystem, globals: Record<string, any>): Loader<unknown> { 51 const moduleKind = ts.getEmitModuleKind(compilerOptions); 52 switch (moduleKind) { 53 case ts.ModuleKind.UMD: 54 case ts.ModuleKind.CommonJS: 55 return new CommonJsLoader(fs, globals); 56 case ts.ModuleKind.System: 57 return new SystemLoader(fs, globals); 58 case ts.ModuleKind.AMD: 59 case ts.ModuleKind.None: 60 default: 61 throw new Error(`ModuleKind '${ts.ModuleKind[moduleKind]}' not supported by evaluator.`); 62 } 63 } 64 65 abstract class Loader<TModule> { 66 protected readonly fs: vfs.FileSystem; 67 protected readonly globals: Record<string, any>; 68 69 private moduleCache = new ts.Map<string, TModule>(); 70 71 constructor(fs: vfs.FileSystem, globals: Record<string, any>) { 72 this.fs = fs; 73 this.globals = globals; 74 } 75 76 protected isFile(file: string) { 77 return this.fs.existsSync(file) && this.fs.statSync(file).isFile(); 78 } 79 80 protected abstract evaluate(text: string, file: string, module: TModule): void; 81 protected abstract createModule(file: string): TModule; 82 protected abstract getExports(module: TModule): any; 83 84 protected load(file: string): TModule { 85 if (!ts.isExternalModuleNameRelative(file)) throw new Error(`Module '${file}' could not be found.`); 86 let module = this.moduleCache.get(file); 87 if (module) return module; 88 this.moduleCache.set(file, module = this.createModule(file)); 89 try { 90 const sourceText = this.fs.readFileSync(file, "utf8"); 91 this.evaluate(sourceText, file, module); 92 return module; 93 } 94 catch (e) { 95 this.moduleCache.delete(file); 96 throw e; 97 } 98 } 99 100 protected resolve(id: string, base: string) { 101 return vpath.resolve(base, id); 102 } 103 104 import(id: string, base = this.fs.cwd()) { 105 if (!ts.isExternalModuleNameRelative(id)) throw new Error(`Module '${id}' could not be found.`); 106 const file = this.resolve(id, base); 107 const module = this.load(file); 108 if (!module) throw new Error(`Module '${id}' could not be found.`); 109 return this.getExports(module); 110 } 111 } 112 113 interface CommonJSModule { 114 exports: any; 115 } 116 117 class CommonJsLoader extends Loader<CommonJSModule> { 118 private resolveAsFile(file: string) { 119 if (this.isFile(file)) return file; 120 if (this.isFile(file + ".js")) return file + ".js"; 121 return undefined; 122 } 123 124 private resolveIndex(dir: string) { 125 const indexFile = vpath.resolve(dir, "index.js"); 126 if (this.isFile(indexFile)) return indexFile; 127 return undefined; 128 } 129 130 private resolveAsDirectory(dir: string) { 131 const packageFile = vpath.resolve(dir, "package.json"); 132 if (this.isFile(packageFile)) { 133 const text = this.fs.readFileSync(packageFile, "utf8"); 134 const json = JSON.parse(text); 135 if (json.main) { 136 const main = vpath.resolve(dir, json.main); 137 const result = this.resolveAsFile(main) || this.resolveIndex(main); 138 if (result === undefined) throw new Error("Module not found"); 139 } 140 } 141 return this.resolveIndex(dir); 142 } 143 144 protected resolve(id: string, base: string) { 145 const file = vpath.resolve(base, id); 146 const resolved = this.resolveAsFile(file) || this.resolveAsDirectory(file); 147 if (!resolved) throw new Error(`Module '${id}' could not be found.`); 148 return resolved; 149 } 150 151 protected createModule(): CommonJSModule { 152 return { exports: {} }; 153 } 154 155 protected getExports(module: CommonJSModule) { 156 return module.exports; 157 } 158 159 protected evaluate(text: string, file: string, module: CommonJSModule): void { 160 const globalNames: string[] = []; 161 const globalArgs: any[] = []; 162 for (const name in this.globals) { 163 if (ts.hasProperty(this.globals, name)) { 164 globalNames.push(name); 165 globalArgs.push(this.globals[name]); 166 } 167 } 168 const base = vpath.dirname(file); 169 const localRequire = (id: string) => this.import(id, base); 170 const evaluateText = `(function (module, exports, require, __dirname, __filename, ${globalNames.join(", ")}) { ${text} })`; 171 // eslint-disable-next-line no-eval 172 const evaluateThunk = (void 0, eval)(evaluateText) as (module: any, exports: any, require: (id: string) => any, dirname: string, filename: string, ...globalArgs: any[]) => void; 173 evaluateThunk.call(this.globals, module, module.exports, localRequire, vpath.dirname(file), file, ...globalArgs); 174 } 175 } 176 177 interface SystemModule { 178 file: string; 179 exports: any; 180 hasExports: boolean; 181 state: SystemModuleState; 182 dependencies: SystemModule[]; 183 dependers: SystemModule[]; 184 setters: SystemModuleDependencySetter[]; 185 requestedDependencies?: string[]; 186 declaration?: SystemModuleDeclaration; 187 hasError?: boolean; 188 error?: any; 189 } 190 191 const enum SystemModuleState { 192 // Instantiation phases: 193 Uninstantiated, 194 Instantiated, 195 196 // Linker phases: 197 AddingDependencies, 198 AllDependenciesAdded, 199 AllDependenciesInstantiated, 200 WiringSetters, 201 Linked, 202 203 // Evaluation phases: 204 Evaluating, 205 Ready, 206 } 207 208 interface SystemModuleExporter { 209 <T>(name: string, value: T): T; 210 <T extends object>(values: T): T; 211 } 212 213 interface SystemModuleContext { 214 import: (id: string) => Promise<any>; 215 meta: any; 216 } 217 218 type SystemModuleRegisterCallback = (exporter: SystemModuleExporter, context: SystemModuleContext) => SystemModuleDeclaration; 219 type SystemModuleDependencySetter = (dependency: any) => void; 220 221 interface SystemModuleDeclaration { 222 setters: SystemModuleDependencySetter[]; 223 execute: () => void; 224 } 225 226 interface SystemGlobal { 227 register(dependencies: string[], declare: SystemModuleRegisterCallback): void; 228 } 229 230 class SystemLoader extends Loader<SystemModule> { 231 protected createModule(file: string): SystemModule { 232 return { 233 file, 234 // eslint-disable-next-line no-null/no-null 235 exports: Object.create(/*o*/ null), 236 dependencies: [], 237 dependers: [], 238 setters: [], 239 hasExports: false, 240 state: SystemModuleState.Uninstantiated 241 }; 242 } 243 244 protected getExports(module: SystemModule) { 245 if (module.state < SystemModuleState.Ready) { 246 this.resetDependers(module, []); 247 this.evaluateModule(module, []); 248 if (module.state < SystemModuleState.Ready) { 249 const error = new Error("Module graph could not be loaded"); 250 this.handleError(module, error); 251 throw error; 252 } 253 } 254 if (module.hasError) { 255 throw module.error; 256 } 257 return module.exports; 258 } 259 260 private handleError(module: SystemModule, error: any) { 261 if (!module.hasError) { 262 module.hasError = true; 263 module.error = error; 264 module.state = SystemModuleState.Ready; 265 } 266 } 267 268 protected evaluate(text: string, _file: string, module: SystemModule): void { 269 const globalNames: string[] = []; 270 const globalArgs: any[] = []; 271 for (const name in this.globals) { 272 if (ts.hasProperty(this.globals, name)) { 273 globalNames.push(name); 274 globalArgs.push(this.globals[name]); 275 } 276 } 277 const localSystem: SystemGlobal = { 278 register: (dependencies, declare) => this.instantiateModule(module, dependencies, declare) 279 }; 280 const evaluateText = `(function (System, ${globalNames.join(", ")}) { ${text} })`; 281 try { 282 // eslint-disable-next-line no-eval 283 const evaluateThunk = (void 0, eval)(evaluateText) as (System: any, ...globalArgs: any[]) => void; 284 evaluateThunk.call(this.globals, localSystem, ...globalArgs); 285 } 286 catch (e) { 287 this.handleError(module, e); 288 throw e; 289 } 290 } 291 292 private instantiateModule(module: SystemModule, dependencies: string[], registration?: SystemModuleRegisterCallback) { 293 function exporter<T>(name: string, value: T): T; 294 function exporter<T>(value: T): T; 295 function exporter<T>(...args: [string, T] | [T]) { 296 module.hasExports = true; 297 const name = args.length === 1 ? undefined : args[0]; 298 const value = args.length === 1 ? args[0] : args[1]; 299 if (name !== undefined) { 300 module.exports[name] = value; 301 } 302 else { 303 for (const name in value) { 304 module.exports[name] = value[name]; 305 } 306 } 307 for (const setter of module.setters) { 308 setter(module.exports); 309 } 310 return value; 311 } 312 313 const context: SystemModuleContext = { 314 import: (_id) => { throw new Error("Dynamic import not implemented."); }, 315 meta: { 316 url: ts.isUrl(module.file) ? module.file : `file:///${ts.normalizeSlashes(module.file).replace(/^\//, "").split("/").map(encodeURIComponent).join("/")}` 317 } 318 }; 319 320 module.requestedDependencies = dependencies; 321 322 try { 323 module.declaration = registration?.(exporter, context); 324 module.state = SystemModuleState.Instantiated; 325 326 for (const depender of module.dependers) { 327 this.linkModule(depender); 328 } 329 330 this.linkModule(module); 331 } 332 catch (e) { 333 this.handleError(module, e); 334 throw e; 335 } 336 } 337 338 private linkModule(module: SystemModule) { 339 try { 340 for (;;) { 341 switch (module.state) { 342 case SystemModuleState.Uninstantiated: { 343 throw new Error("Module not yet instantiated"); 344 } 345 case SystemModuleState.Instantiated: { 346 // Module has been instantiated, start requesting dependencies. 347 // Set state so that re-entry while adding dependencies does nothing. 348 module.state = SystemModuleState.AddingDependencies; 349 const base = vpath.dirname(module.file); 350 const dependencies = module.requestedDependencies || []; 351 352 for (const dependencyId of dependencies) { 353 const dependency = this.load(this.resolve(dependencyId, base)); 354 module.dependencies.push(dependency); 355 dependency.dependers.push(module); 356 } 357 358 // All dependencies have been added, switch state 359 // to check whether all dependencies are instantiated 360 module.state = SystemModuleState.AllDependenciesAdded; 361 continue; 362 } 363 case SystemModuleState.AddingDependencies: { 364 // in the middle of adding dependencies for this module, do nothing 365 return; 366 } 367 case SystemModuleState.AllDependenciesAdded: { 368 // all dependencies have been added, advance state if all dependencies are instantiated. 369 for (const dependency of module.dependencies) { 370 if (dependency.state === SystemModuleState.Uninstantiated) { 371 return; 372 } 373 } 374 375 // indicate all dependencies are instantiated for this module. 376 module.state = SystemModuleState.AllDependenciesInstantiated; 377 378 // trigger links for dependers of this module. 379 for (const depender of module.dependers) { 380 this.linkModule(depender); 381 } 382 continue; 383 } 384 case SystemModuleState.AllDependenciesInstantiated: { 385 // all dependencies have been instantiated, start wiring setters 386 module.state = SystemModuleState.WiringSetters; 387 for (let i = 0; i < module.dependencies.length; i++) { 388 const dependency = module.dependencies[i]; 389 const setter = module.declaration?.setters[i]; 390 if (setter) { 391 dependency.setters.push(setter); 392 if (dependency.hasExports || dependency.state === SystemModuleState.Ready) { 393 // wire hoisted exports or ready dependencies. 394 setter(dependency.exports); 395 } 396 } 397 } 398 399 module.state = SystemModuleState.Linked; 400 401 // ensure graph is fully linked 402 for (const depender of module.dependers) { 403 this.linkModule(depender); 404 } 405 continue; 406 } 407 408 case SystemModuleState.WiringSetters: // in the middle of wiring setters for this module, nothing to do 409 case SystemModuleState.Linked: // module has already been linked, nothing to do 410 case SystemModuleState.Evaluating: // module is currently evaluating, nothing to do 411 case SystemModuleState.Ready: // module is done evaluating, nothing to do 412 return; 413 } 414 } 415 } 416 catch (e) { 417 this.handleError(module, e); 418 throw e; 419 } 420 } 421 422 private resetDependers(module: SystemModule, stack: SystemModule[]) { 423 if (stack.lastIndexOf(module) !== -1) { 424 return; 425 } 426 427 stack.push(module); 428 module.dependers.length = 0; 429 for (const dependency of module.dependencies) { 430 this.resetDependers(dependency, stack); 431 } 432 stack.pop(); 433 } 434 435 private evaluateModule(module: SystemModule, stack: SystemModule[]) { 436 if (module.state < SystemModuleState.Linked) throw new Error("Invalid state for evaluation."); 437 if (module.state !== SystemModuleState.Linked) return; 438 439 if (stack.lastIndexOf(module) !== -1) { 440 // we are already evaluating this module 441 return; 442 } 443 444 stack.push(module); 445 module.state = SystemModuleState.Evaluating; 446 try { 447 for (const dependency of module.dependencies) { 448 this.evaluateModule(dependency, stack); 449 } 450 module.declaration?.execute?.(); 451 module.state = SystemModuleState.Ready; 452 } 453 catch (e) { 454 this.handleError(module, e); 455 throw e; 456 } 457 } 458 } 459} 460