Skip to content

Commit 78946fe

Browse files
committed
feat(offline compiler): a replacement for tsc that compiles templates
see angular#7483.
1 parent 33e53c9 commit 78946fe

File tree

17 files changed

+743
-23
lines changed

17 files changed

+743
-23
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ tmp
2121
*.js.deps
2222
*.js.map
2323

24+
# Files created by the template compiler
25+
**/*.ngfactory.ts
26+
2427
# Or type definitions we mirror from github
2528
# (NB: these lines are removed in publish-build-artifacts.sh)
2629
**/typings/**/*.d.ts

gulpfile.js

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -452,22 +452,40 @@ gulp.task('serve.e2e.dart', ['build.js.cjs'], function(neverDone) {
452452
// ------------------
453453
// CI tests suites
454454

455-
function runKarma(configFile, done) {
455+
function execProcess(name, args, done) {
456456
var exec = require('child_process').exec;
457457

458-
var cmd = process.platform === 'win32' ? 'node_modules\\.bin\\karma run ' :
459-
'node node_modules/.bin/karma run ';
460-
cmd += configFile;
461-
exec(cmd, function(e, stdout) {
458+
var cmd = process.platform === 'win32' ? 'node_modules\\.bin\\' + name + ' ' :
459+
'node node_modules/.bin/' + name + ' ';
460+
cmd += args;
461+
exec(cmd, done);
462+
}
463+
function runKarma(configFile, done) {
464+
execProcess('karma', 'run ' + configFile, function(e, stdout) {
462465
// ignore errors, we don't want to fail the build in the interactive (non-ci) mode
463466
// karma server will print all test failures
464467
done();
465468
});
466469
}
467470

471+
// Gulp-typescript doesn't work with typescript@next:
472+
// https://github.com/ivogabe/gulp-typescript/issues/331
473+
function runTsc(project, done) {
474+
execProcess('tsc', '-p ' + project, function(e, stdout, stderr) {
475+
if (e) {
476+
console.log(stdout);
477+
console.error(stderr);
478+
done(e);
479+
} else {
480+
done();
481+
}
482+
});
483+
}
484+
468485
gulp.task('test.js', function(done) {
469486
runSequence('test.unit.tools/ci', 'test.transpiler.unittest', 'test.unit.js/ci',
470-
'test.unit.cjs/ci', 'test.typings', 'check-public-api', sequenceComplete(done));
487+
'test.unit.cjs/ci', 'test.compiler_cli', 'test.typings', 'check-public-api',
488+
sequenceComplete(done));
471489
});
472490

473491
gulp.task('test.dart', function(done) {
@@ -768,7 +786,7 @@ gulp.task('!checkAndReport.payload.js', function() {
768786
{
769787
failConditions: PAYLOAD_TESTS_CONFIG.ts[packaging].sizeLimits,
770788
prefix: caseName + '_' + packaging
771-
})
789+
});
772790
}
773791

774792
return PAYLOAD_TESTS_CONFIG.ts.cases.reduce(function(sizeReportingStreams, caseName) {
@@ -1026,6 +1044,26 @@ gulp.task('!test.typings',
10261044
gulp.task('test.typings', ['build.js.cjs'],
10271045
function(done) { runSequence('!test.typings', sequenceComplete(done)); });
10281046

1047+
gulp.task('!build.compiler_cli', ['build.js.cjs'],
1048+
function(done) { runTsc('tools/compiler_cli/src', done); });
1049+
1050+
gulp.task('!test.compiler_cli.codegen', function(done) {
1051+
try {
1052+
require('./dist/js/cjs/compiler_cli')
1053+
.main("tools/compiler_cli/test")
1054+
.then(function() { done(); })
1055+
.catch(function(rej) { done(new Error(rej)); });
1056+
} catch (err) {
1057+
done(err);
1058+
}
1059+
});
1060+
1061+
// End-to-end test for compiler CLI.
1062+
// Calls the compiler using its command-line interface, then compiles the app with the codegen.
1063+
// TODO(alexeagle): wire up the playground tests with offline compilation, similar to dart.
1064+
gulp.task('test.compiler_cli', ['!build.compiler_cli'],
1065+
function(done) { runSequence('!test.compiler_cli.codegen', sequenceComplete(done)); });
1066+
10291067
// -----------------
10301068
// orchestrated targets
10311069

@@ -1091,7 +1129,7 @@ gulp.task('!build.tools', function() {
10911129
var sourcemaps = require('gulp-sourcemaps');
10921130
var tsc = require('gulp-typescript');
10931131

1094-
var stream = gulp.src(['tools/**/*.ts'])
1132+
var stream = gulp.src(['tools/**/*.ts', '!tools/compiler_cli/**'])
10951133
.pipe(sourcemaps.init())
10961134
.pipe(tsc({
10971135
target: 'ES5',
@@ -1512,7 +1550,7 @@ gulp.on('task_start', (e) => {
15121550
analytics.buildSuccess('gulp <startup>', process.uptime() * 1000);
15131551
}
15141552

1515-
analytics.buildStart('gulp ' + e.task)
1553+
analytics.buildStart('gulp ' + e.task);
15161554
});
1517-
gulp.on('task_stop', (e) => {analytics.buildSuccess('gulp ' + e.task, e.duration * 1000)});
1518-
gulp.on('task_err', (e) => {analytics.buildError('gulp ' + e.task, e.duration * 1000)});
1555+
gulp.on('task_stop', (e) => { analytics.buildSuccess('gulp ' + e.task, e.duration * 1000); });
1556+
gulp.on('task_err', (e) => { analytics.buildError('gulp ' + e.task, e.duration * 1000); });

tools/build/linknodemodules.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ var fs = require('fs');
22
var path = require('path');
33

44
module.exports = function(gulp, plugins, config) {
5+
function symlink(relativeFolder, linkDir) {
6+
var sourceDir = path.join('..', relativeFolder);
7+
if (!fs.existsSync(linkDir)) {
8+
console.log('creating link', linkDir, sourceDir);
9+
try {
10+
fs.symlinkSync(sourceDir, linkDir, 'dir');
11+
}
12+
catch(e) {
13+
var sourceDir = path.join(config.dir, relativeFolder);
14+
console.log('linking failed: trying to hard copy', linkDir, sourceDir);
15+
copyRecursiveSync(sourceDir, linkDir);
16+
}
17+
}
18+
}
19+
520
return function() {
621
var nodeModulesDir = path.join(config.dir, 'node_modules');
722
if (!fs.existsSync(nodeModulesDir)) {
@@ -11,20 +26,12 @@ module.exports = function(gulp, plugins, config) {
1126
if (relativeFolder === 'node_modules') {
1227
return;
1328
}
14-
var sourceDir = path.join('..', relativeFolder);
29+
1530
var linkDir = path.join(nodeModulesDir, relativeFolder);
16-
if (!fs.existsSync(linkDir)) {
17-
console.log('creating link', linkDir, sourceDir);
18-
try {
19-
fs.symlinkSync(sourceDir, linkDir, 'dir');
20-
}
21-
catch(e) {
22-
var sourceDir = path.join(config.dir, relativeFolder);
23-
console.log('linking failed: trying to hard copy', linkDir, sourceDir);
24-
copyRecursiveSync(sourceDir, linkDir);
25-
}
26-
}
31+
symlink(relativeFolder, linkDir);
2732
});
33+
// Also symlink tools we release independently to NPM, so tests can require metadata, etc.
34+
symlink('../../tools/metadata', path.join(nodeModulesDir, 'ts-metadata-collector'));
2835
};
2936
};
3037

tools/compiler_cli/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Angular Template Compiler
2+
3+
Angular applications are built with templates, which may be `.html` or `.css` files,
4+
or may be inline `template` attributes on Decorators like `@Component`.
5+
6+
These templates are compiled into executable JS at application runtime (except in `interpretation` mode).
7+
This compilation can occur on the client, but it results in slower bootstrap time, and also
8+
requires that the compiler be included in the code downloaded to the client.
9+
10+
You can produce smaller, faster applications by running Angular's compiler as a build step,
11+
and then downloading only the executable JS to the client.
12+
13+
## Configuration
14+
15+
The `tsconfig.json` file is expected to contain an additional configuration block:
16+
```
17+
"angularCompilerOptions": {
18+
"genDir": "."
19+
}
20+
```
21+
the `genDir` option controls the path (relative to `tsconfig.json`) where the generated file tree
22+
will be written. More options may be added as we implement more features.
23+
24+
We recommend you avoid checking generated files into version control. This permits a state where
25+
the generated files in the repository were created from sources that were never checked in,
26+
making it impossible to reproduce the current state. Also, your changes will effectively appear
27+
twice in code reviews, with the generated version inscrutible by the reviewer.
28+
29+
In TypeScript 1.8, the generated sources will have to be written alongside your originals,
30+
so set `genDir` to the same location as your files (typicially the same as `rootDir`).
31+
Add `**/*.ngfactory.ts` to your `.gitignore` or other mechanism for your version control system.
32+
33+
In TypeScript 1.9 and above, you can add a generated folder into your application,
34+
such as `codegen`. Using the `rootDirs` option, you can allow relative imports like
35+
`import {} from './foo.ngfactory'` even though the `src` and `codegen` trees are distinct.
36+
Add `**/codegen` to your `.gitignore` or similar.
37+
38+
Note that in the second option, TypeScript will emit the code into two parallel directories
39+
as well. This is by design, see https://github.com/Microsoft/TypeScript/issues/8245.
40+
This makes the configuration of your runtime module loader more complex, so we don't recommend
41+
this option yet.
42+
43+
See the example in the `test/` directory for a working example.
44+
45+
## Compiler CLI
46+
47+
This program mimics the TypeScript tsc command line. It accepts a `-p` flag which points to a
48+
`tsconfig.json` file, or a directory containing one.
49+
50+
This CLI is intended for demos, prototyping, or for users with simple build systems
51+
that run bare `tsc`.
52+
53+
Users with a build system should expect an Angular 2 template plugin. Such a plugin would be
54+
based on the `index.ts` in this directory, but should share the TypeScript compiler instance
55+
with the one already used in the plugin for TypeScript typechecking and emit.
56+
57+
## Design
58+
At a high level, this program
59+
- collects static metadata about the sources using the `ts-metadata-collector` package in angular2
60+
- uses the `OfflineCompiler` from `angular2/src/compiler/compiler` to codegen additional `.ts` files
61+
- these `.ts` files are written to the `genDir` path, then compiled together with the application.
62+
63+
## For developers
64+
Run the compiler from source:
65+
```
66+
# Build angular2
67+
gulp build.js.cjs
68+
# Build the compiler
69+
./node_modules/.bin/tsc -p tools/compiler_cli/src
70+
# Run it on the test project
71+
node ./dist/js/cjs/compiler_cli -p tools/compiler_cli/test
72+
```

tools/compiler_cli/src/codegen.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Transform template html and css into executable code.
3+
* Intended to be used in a build step.
4+
*/
5+
import * as ts from 'typescript';
6+
import * as path from 'path';
7+
8+
import * as compiler from 'angular2/compiler';
9+
import {StaticReflector} from 'angular2/src/compiler/static_reflector';
10+
import {CompileMetadataResolver} from 'angular2/src/compiler/metadata_resolver';
11+
import {HtmlParser} from 'angular2/src/compiler/html_parser';
12+
import {DirectiveNormalizer} from 'angular2/src/compiler/directive_normalizer';
13+
import {Lexer} from 'angular2/src/compiler/expression_parser/lexer';
14+
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
15+
import {TemplateParser} from 'angular2/src/compiler/template_parser';
16+
import {DomElementSchemaRegistry} from 'angular2/src/compiler/schema/dom_element_schema_registry';
17+
import {StyleCompiler} from 'angular2/src/compiler/style_compiler';
18+
import {ViewCompiler} from 'angular2/src/compiler/view_compiler/view_compiler';
19+
import {TypeScriptEmitter} from 'angular2/src/compiler/output/ts_emitter';
20+
import {RouterLinkTransform} from 'angular2/src/router/directives/router_link_transform';
21+
import {Parse5DomAdapter} from 'angular2/platform/server';
22+
23+
import {MetadataCollector} from 'ts-metadata-collector';
24+
import {NodeReflectorHost} from './reflector_host';
25+
import {wrapCompilerHost, CodeGeneratorHost} from './compiler_host';
26+
27+
const SOURCE_EXTENSION = /\.[jt]s$/;
28+
const PREAMBLE = `/**
29+
* This file is generated by the Angular 2 template compiler.
30+
* Do not edit.
31+
*/
32+
`;
33+
34+
export interface AngularCompilerOptions {
35+
// Absolute path to a directory where generated file structure is written
36+
genDir: string;
37+
}
38+
39+
export class CodeGenerator {
40+
constructor(private ngOptions: AngularCompilerOptions, private basePath: string,
41+
public program: ts.Program, public host: CodeGeneratorHost,
42+
private staticReflector: StaticReflector, private resolver: CompileMetadataResolver,
43+
private compiler: compiler.OfflineCompiler,
44+
private reflectorHost: NodeReflectorHost) {}
45+
46+
private generateSource(metadatas: compiler.CompileDirectiveMetadata[]) {
47+
const normalize = (metadata: compiler.CompileDirectiveMetadata) => {
48+
const directiveType = metadata.type.runtime;
49+
const directives = this.resolver.getViewDirectivesMetadata(directiveType);
50+
const pipes = this.resolver.getViewPipesMetadata(directiveType);
51+
return new compiler.NormalizedComponentWithViewDirectives(metadata, directives, pipes);
52+
};
53+
54+
return this.compiler.compileTemplates(metadatas.map(normalize));
55+
}
56+
57+
private readComponents(absSourcePath: string) {
58+
const result: Promise<compiler.CompileDirectiveMetadata>[] = [];
59+
const metadata = this.staticReflector.getModuleMetadata(absSourcePath);
60+
if (!metadata) {
61+
console.log(`WARNING: no metadata found for ${absSourcePath}`);
62+
return result;
63+
}
64+
65+
const symbols = Object.keys(metadata['metadata']);
66+
if (!symbols || !symbols.length) {
67+
return result;
68+
}
69+
for (const symbol of symbols) {
70+
const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
71+
let directive: compiler.CompileDirectiveMetadata;
72+
directive = this.resolver.maybeGetDirectiveMetadata(<any>staticType);
73+
74+
if (!directive || !directive.isComponent) {
75+
continue;
76+
}
77+
result.push(this.compiler.normalizeDirectiveMetadata(directive));
78+
}
79+
return result;
80+
}
81+
82+
codegen() {
83+
Parse5DomAdapter.makeCurrent();
84+
const generateOneFile = (absSourcePath: string) =>
85+
Promise.all(this.readComponents(absSourcePath))
86+
.then((metadatas: compiler.CompileDirectiveMetadata[]) => {
87+
if (!metadatas || !metadatas.length) {
88+
return;
89+
}
90+
const generated = this.generateSource(metadatas);
91+
const sourceFile = this.program.getSourceFile(absSourcePath);
92+
93+
// Write codegen in a directory structure matching the sources.
94+
// TODO(alexeagle): maybe use generated.moduleUrl instead of hardcoded ".ngfactory.ts"
95+
// TODO(alexeagle): relativize paths by the rootDirs option
96+
const emitPath =
97+
path.join(this.ngOptions.genDir, path.relative(this.basePath, absSourcePath))
98+
.replace(SOURCE_EXTENSION, '.ngfactory.ts');
99+
this.host.writeFile(emitPath, PREAMBLE + generated.source, false, () => {},
100+
[sourceFile]);
101+
})
102+
.catch((e) => { console.error(e.stack); });
103+
104+
return Promise.all(this.program.getRootFileNames()
105+
.filter(f => !/\.ngfactory\.ts$/.test(f))
106+
.map(generateOneFile));
107+
}
108+
109+
static create(ngOptions: AngularCompilerOptions, parsed: ts.ParsedCommandLine, basePath: string,
110+
compilerHost: ts.CompilerHost):
111+
{errors?: ts.Diagnostic[], generator?: CodeGenerator} {
112+
const program = ts.createProgram(parsed.fileNames, parsed.options, compilerHost);
113+
const errors = program.getOptionsDiagnostics();
114+
if (errors && errors.length) {
115+
return {errors};
116+
}
117+
118+
const metadataCollector = new MetadataCollector();
119+
const reflectorHost =
120+
new NodeReflectorHost(program, metadataCollector, compilerHost, parsed.options);
121+
const xhr: compiler.XHR = {get: (s: string) => Promise.resolve(compilerHost.readFile(s))};
122+
const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
123+
const staticReflector = new StaticReflector(reflectorHost);
124+
const htmlParser = new HtmlParser();
125+
const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser);
126+
const parser = new Parser(new Lexer());
127+
const tmplParser = new TemplateParser(parser, new DomElementSchemaRegistry(), htmlParser,
128+
/*console*/ null, [new RouterLinkTransform(parser)]);
129+
const offlineCompiler = new compiler.OfflineCompiler(
130+
normalizer, tmplParser, new StyleCompiler(urlResolver),
131+
new ViewCompiler(new compiler.CompilerConfig(true, true, true)), new TypeScriptEmitter());
132+
const resolver = new CompileMetadataResolver(
133+
new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector),
134+
new compiler.ViewResolver(staticReflector), null, null, staticReflector);
135+
136+
return {
137+
generator: new CodeGenerator(ngOptions, basePath, program,
138+
wrapCompilerHost(compilerHost, parsed.options), staticReflector,
139+
resolver, offlineCompiler, reflectorHost)
140+
};
141+
}
142+
}

0 commit comments

Comments
 (0)