Skip to content

Commit dcab681

Browse files
committed
Inline TypeScript compiler for rendering from source files
Implement inline TypeScript compiler that we can use to extract module definitions from Angular CLI projects that cannot define additional webpack targets. The next step is to integrate the offline compiler to generate NgFactory files and use those to instantiate the renderer.
1 parent 507d511 commit dcab681

File tree

12 files changed

+239
-44
lines changed

12 files changed

+239
-44
lines changed

source/application/compiler/bundle.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 105 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,110 @@
11
import {
2+
NgModuleFactory,
3+
NgModule
4+
} from '@angular/core';
5+
6+
import {
7+
CompilerOptions,
28
Diagnostic,
3-
FormatDiagnosticsHost,
9+
ModuleKind,
10+
ModuleResolutionKind,
411
Program,
5-
SourceFile,
612
WriteFileCallback,
713
createCompilerHost,
814
createProgram,
9-
formatDiagnostics,
1015
getPreEmitDiagnostics,
1116
} from 'typescript';
1217

13-
import {EOL} from 'os';
14-
import {cwd} from 'process';
15-
import {relative} from 'path';
18+
import {dirname} from 'path';
1619

17-
import {ApplicationBundle} from './bundle';
18-
import {CompilerOptions, loadProjectOptions} from './options';
20+
import {CompileOptions, loadProjectOptions} from './options';
1921
import {CompilerException} from 'exception';
2022
import {Project} from '../project';
21-
23+
import {Reflector} from 'platform';
24+
import {VirtualMachine} from './vm';
25+
import {diagnosticsToException} from './diagnostics';
2226
import {flatten} from 'transformation';
2327

2428
export class Compiler {
25-
private options: CompilerOptions;
29+
private options: CompileOptions;
2630

27-
constructor(project: Project) {
31+
constructor(private project: Project) {
2832
this.options = loadProjectOptions(project);
33+
34+
if (project.ngModule == null ||
35+
project.ngModule.length < 2) {
36+
throw new CompilerException('Compiler requires a module ID and an export name in ngModule');
37+
}
38+
}
39+
40+
compile(): NgModuleFactory<any> {
41+
const vm = new VirtualMachine();
42+
try {
43+
this.compileToVm(vm);
44+
45+
const [moduleId, exported] = this.project.ngModule;
46+
47+
const requiredModule = vm.require(moduleId);
48+
if (requiredModule == null) {
49+
throw new CompilerException(`Attempted to require ${moduleId} but received a null or undefined object`);
50+
}
51+
52+
const rootModule =
53+
!exported
54+
? requiredModule
55+
: requiredModule[exported];
56+
57+
if (Reflector.decorated(rootModule, NgModule) === false) {
58+
throw new CompilerException(`Root module type ${rootModule.name} is not decorated with @NgModule`);
59+
}
60+
61+
return rootModule;
62+
}
63+
finally {
64+
vm.dispose();
65+
}
2966
}
3067

31-
async compile(): Promise<ApplicationBundle> {
68+
private compileToVm(vm: VirtualMachine) {
3269
const program = this.createProgram(null);
3370

34-
return new Promise(resolve => {
35-
const writer: WriteFileCallback =
36-
(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: SourceFile[]) => {
37-
throw new CompilerException('Not implemented');
38-
};
71+
const compilerOptions = program.getCompilerOptions();
3972

40-
program.emit(undefined, writer, null, false);
41-
});
73+
const writer: WriteFileCallback =
74+
(fileName, data, writeByteOrderMark, onError?, sourceFiles?) => {
75+
try {
76+
const moduleId = this.moduleIdFromFilename(fileName, compilerOptions);
77+
78+
vm.define(fileName, moduleId, data);
79+
}
80+
catch (exception) {
81+
if (onError == null) {
82+
throw exception;
83+
}
84+
onError(exception.stack);
85+
}
86+
};
87+
88+
program.emit(undefined, writer, null, false);
4289
}
4390

4491
private createProgram(previousProgram?: Program): Program {
4592
const {typescriptOptions} = this.options;
4693

47-
const compilerHost = createCompilerHost(typescriptOptions.options, true);
94+
const options = Object.assign({}, typescriptOptions.options, {
95+
declaration: false,
96+
sourceMap: false,
97+
sourceRoot: null,
98+
inlineSourceMap: false,
99+
module: ModuleKind.CommonJS,
100+
moduleResolution: ModuleResolutionKind.NodeJs,
101+
});
102+
103+
const compilerHost = createCompilerHost(options, true);
48104

49105
const program = createProgram(
50106
typescriptOptions.fileNames,
51-
typescriptOptions.options,
107+
options,
52108
compilerHost,
53109
previousProgram);
54110

@@ -64,18 +120,37 @@ export class Compiler {
64120
}
65121

66122
private conditionalException(diagnostics: Array<Diagnostic>) {
67-
if (diagnostics == null || diagnostics.length === 0) {
123+
if (diagnostics == null ||
124+
diagnostics.length === 0) {
68125
return;
69126
}
127+
throw new CompilerException(diagnosticsToException(diagnostics));
128+
}
70129

71-
const host: FormatDiagnosticsHost = {
72-
getCurrentDirectory: (): string => cwd(),
73-
getCanonicalFileName: (filename: string): string => relative(cwd(), filename),
74-
getNewLine: (): string => EOL,
75-
};
76-
77-
const formatted = formatDiagnostics(diagnostics, host);
130+
private moduleIdFromFilename(filename: string, compilerOptions: CompilerOptions): string {
131+
const projectPath = (path: string): string =>
132+
path.toLowerCase().endsWith('.json')
133+
? dirname(path)
134+
: path;
135+
136+
const candidates = [
137+
compilerOptions.baseUrl,
138+
compilerOptions.outDir,
139+
compilerOptions.rootDir,
140+
compilerOptions.project
141+
? projectPath(compilerOptions.project)
142+
: null,
143+
].concat(compilerOptions.rootDirs || []).filter(v => v);
144+
145+
const lowerfile = filename.toLowerCase();
146+
147+
const matches = candidates.map(v => v.toLowerCase()).filter(p => lowerfile.startsWith(p));
148+
if (matches.length > 0) {
149+
return filename.substring(matches[0].length)
150+
.replace(/\.js$/, String())
151+
.replace(/^\//, String());
152+
}
78153

79-
throw new CompilerException(formatted);
154+
throw new CompilerException(`Cannot determine module ID of file ${filename}`);
80155
}
81156
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {EOL} from 'os';
2+
import {cwd} from 'process';
3+
4+
import {
5+
Diagnostic,
6+
FormatDiagnosticsHost,
7+
formatDiagnostics
8+
} from 'typescript';
9+
10+
export const diagnosticsToException = (diagnostics: Array<Diagnostic>): string => {
11+
const host: FormatDiagnosticsHost = {
12+
getCurrentDirectory: (): string => cwd(),
13+
getCanonicalFileName: (filename: string): string => filename,
14+
getNewLine: (): string => EOL,
15+
};
16+
17+
return formatDiagnostics(diagnostics, host);
18+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export * from './bundle';
1+
export * from './vm';
22
export * from './compiler';
33
export * from './options';

source/application/compiler/options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import {ParsedCommandLine} from 'typescript';
55

66
import {Project} from '../project';
77

8-
export interface CompilerOptions {
8+
export interface CompileOptions {
99
// TypeScript compiler options
1010
typescriptOptions: ParsedCommandLine;
1111

1212
// Angular compiler options
1313
angularOptions: AngularCompilerOptions;
1414
}
1515

16-
export const loadProjectOptions = (project: Project): CompilerOptions => {
16+
export const loadProjectOptions = (project: Project): CompileOptions => {
1717
const tsc = new Tsc();
1818

1919
const {parsed, ngOptions} = tsc.readConfiguration(project.tsconfig, project.basePath);

source/application/compiler/tests/compiler.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ describe('Compiler', () => {
1010

1111
return new Compiler({
1212
basePath: dirname(tsconfig),
13-
ngModule: ['test-fixtures/application-basic-inline.ts', 'BasicInlineApplication'],
13+
ngModule: ['test-fixtures/application-basic-inline', 'BasicInlineApplication'],
1414
tsconfig,
1515
});
1616
};
1717

18-
xit('should be able to build a TypeScript application and produce in-memory artifacts', async () => {
18+
it('should be able to build a TypeScript application and produce in-memory artifacts', async () => {
1919
const compiler = createCompiler();
20-
const bundle = await compiler.compile();
21-
expect(bundle).not.toBeNull();
20+
const module = await compiler.compile();
21+
expect(module).not.toBeNull();
22+
expect(typeof module).toBe('function');
2223
});
2324
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './virtual-machine';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {VirtualMachine} from '../virtual-machine';
2+
3+
describe('VirtualMachine', () => {
4+
it('can define multiple sandboxed scripts and execute them', () => {
5+
const vm = new VirtualMachine();
6+
7+
vm.define('/foo/m1.js', 'm1', 'module.exports = {foo: function() { return 1; }};');
8+
vm.define('/foo/m2.js', 'm2', `return require('./m1')`);
9+
10+
const result = vm.require('./m1');
11+
expect(result).not.toBeNull();
12+
expect(result.foo).not.toBeNull();
13+
expect(typeof result.foo).toBe('function');
14+
expect(result.foo()).toBe(1);
15+
});
16+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {Script, createContext} from 'vm';
2+
3+
import {dirname, join, normalize} from 'path';
4+
5+
import {Disposable} from 'disposable';
6+
7+
import {VirtualMachineException} from 'exception';
8+
9+
export class VirtualMachine implements Disposable {
10+
private scripts = new Map<string, Script>();
11+
private modules = new Map<string, any>();
12+
13+
define(filename: string, moduleId: string, code: string) {
14+
if (moduleId.startsWith('/') ||
15+
moduleId.startsWith('.')) {
16+
throw new VirtualMachineException(`Invalid module ID: ${moduleId}`);
17+
}
18+
19+
if (this.scripts.has(moduleId)) {
20+
throw new VirtualMachineException(`Cannot overwrite existing module ${moduleId}`);
21+
}
22+
23+
const wrappedCode = `(function() {
24+
const module = {id: '${moduleId}', exports: {}, filename: '${filename}'};
25+
const exports = module.exports;
26+
${code};
27+
return exports;
28+
})()`;
29+
30+
const script = new Script(wrappedCode, {filename, displayErrors: true});
31+
32+
this.scripts.set(moduleId, script);
33+
}
34+
35+
require(moduleId: string, relativeTo?: string) {
36+
const absolutePath = this.resolvePath(moduleId, relativeTo);
37+
38+
if (this.modules.has(absolutePath) === false) {
39+
const script = this.scripts.get(absolutePath);
40+
if (script == null) {
41+
return require(moduleId); // probably a third-party module
42+
}
43+
44+
const context = createContext({require: mid => this.require(mid, moduleId)});
45+
46+
try {
47+
this.modules.set(moduleId, script.runInContext(context));
48+
}
49+
catch (exception) {
50+
throw new VirtualMachineException(`Failure to execute ${moduleId} in VM: ${exception.stack}`, exception);
51+
}
52+
}
53+
54+
return this.modules.get(moduleId);
55+
}
56+
57+
dispose() {
58+
this.scripts.clear();
59+
this.modules.clear();
60+
}
61+
62+
private resolvePath(to: string, from: string) {
63+
if (to.startsWith('/')) {
64+
throw new VirtualMachineException(`Cannot resolve a path that starts with /: ${to} (from ${from})`);
65+
}
66+
if (from) {
67+
return normalize(join(dirname(from), to));
68+
}
69+
return normalize(to);
70+
}
71+
}

source/exception.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
export class Exception extends Error {
2-
constructor(msg: string, private innerException?: Error) {
3-
super(msg);
4-
}
2+
private innerException: Error;
3+
4+
constructor(msg: string, innerException?: Error) {
5+
if (innerException) {
6+
super(`${msg} -> ${innerException.stack}`);
7+
}
8+
else {
9+
super(msg);
10+
}
11+
12+
this.innerException = innerException;
13+
}
514

615
public get stack(): string {
716
if (this.innerException) {
@@ -20,4 +29,6 @@ export class PlatformException extends Exception {}
2029
export class RendererException extends Exception {}
2130
export class ResourceException extends Exception {}
2231
export class RouteException extends Exception {}
23-
export class SnapshotException extends Exception {}
32+
export class SnapshotException extends Exception {}
33+
export class MemoryFilesystemException extends Exception {}
34+
export class VirtualMachineException extends Exception {}

0 commit comments

Comments
 (0)