• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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