feat(offline compiler): a replacement for tsc that compiles templates

see #7483.
This commit is contained in:
Alex Eagle 2016-04-28 21:57:16 -07:00
parent 33e53c9a59
commit 78946fe9fa
17 changed files with 743 additions and 23 deletions

3
.gitignore vendored
View File

@ -21,6 +21,9 @@ tmp
*.js.deps
*.js.map
# Files created by the template compiler
**/*.ngfactory.ts
# Or type definitions we mirror from github
# (NB: these lines are removed in publish-build-artifacts.sh)
**/typings/**/*.d.ts

View File

@ -452,22 +452,40 @@ gulp.task('serve.e2e.dart', ['build.js.cjs'], function(neverDone) {
// ------------------
// CI tests suites
function runKarma(configFile, done) {
function execProcess(name, args, done) {
var exec = require('child_process').exec;
var cmd = process.platform === 'win32' ? 'node_modules\\.bin\\karma run ' :
'node node_modules/.bin/karma run ';
cmd += configFile;
exec(cmd, function(e, stdout) {
var cmd = process.platform === 'win32' ? 'node_modules\\.bin\\' + name + ' ' :
'node node_modules/.bin/' + name + ' ';
cmd += args;
exec(cmd, done);
}
function runKarma(configFile, done) {
execProcess('karma', 'run ' + configFile, function(e, stdout) {
// ignore errors, we don't want to fail the build in the interactive (non-ci) mode
// karma server will print all test failures
done();
});
}
// Gulp-typescript doesn't work with typescript@next:
// https://github.com/ivogabe/gulp-typescript/issues/331
function runTsc(project, done) {
execProcess('tsc', '-p ' + project, function(e, stdout, stderr) {
if (e) {
console.log(stdout);
console.error(stderr);
done(e);
} else {
done();
}
});
}
gulp.task('test.js', function(done) {
runSequence('test.unit.tools/ci', 'test.transpiler.unittest', 'test.unit.js/ci',
'test.unit.cjs/ci', 'test.typings', 'check-public-api', sequenceComplete(done));
'test.unit.cjs/ci', 'test.compiler_cli', 'test.typings', 'check-public-api',
sequenceComplete(done));
});
gulp.task('test.dart', function(done) {
@ -768,7 +786,7 @@ gulp.task('!checkAndReport.payload.js', function() {
{
failConditions: PAYLOAD_TESTS_CONFIG.ts[packaging].sizeLimits,
prefix: caseName + '_' + packaging
})
});
}
return PAYLOAD_TESTS_CONFIG.ts.cases.reduce(function(sizeReportingStreams, caseName) {
@ -1026,6 +1044,26 @@ gulp.task('!test.typings',
gulp.task('test.typings', ['build.js.cjs'],
function(done) { runSequence('!test.typings', sequenceComplete(done)); });
gulp.task('!build.compiler_cli', ['build.js.cjs'],
function(done) { runTsc('tools/compiler_cli/src', done); });
gulp.task('!test.compiler_cli.codegen', function(done) {
try {
require('./dist/js/cjs/compiler_cli')
.main("tools/compiler_cli/test")
.then(function() { done(); })
.catch(function(rej) { done(new Error(rej)); });
} catch (err) {
done(err);
}
});
// End-to-end test for compiler CLI.
// Calls the compiler using its command-line interface, then compiles the app with the codegen.
// TODO(alexeagle): wire up the playground tests with offline compilation, similar to dart.
gulp.task('test.compiler_cli', ['!build.compiler_cli'],
function(done) { runSequence('!test.compiler_cli.codegen', sequenceComplete(done)); });
// -----------------
// orchestrated targets
@ -1091,7 +1129,7 @@ gulp.task('!build.tools', function() {
var sourcemaps = require('gulp-sourcemaps');
var tsc = require('gulp-typescript');
var stream = gulp.src(['tools/**/*.ts'])
var stream = gulp.src(['tools/**/*.ts', '!tools/compiler_cli/**'])
.pipe(sourcemaps.init())
.pipe(tsc({
target: 'ES5',
@ -1512,7 +1550,7 @@ gulp.on('task_start', (e) => {
analytics.buildSuccess('gulp <startup>', process.uptime() * 1000);
}
analytics.buildStart('gulp ' + e.task)
analytics.buildStart('gulp ' + e.task);
});
gulp.on('task_stop', (e) => {analytics.buildSuccess('gulp ' + e.task, e.duration * 1000)});
gulp.on('task_err', (e) => {analytics.buildError('gulp ' + e.task, e.duration * 1000)});
gulp.on('task_stop', (e) => { analytics.buildSuccess('gulp ' + e.task, e.duration * 1000); });
gulp.on('task_err', (e) => { analytics.buildError('gulp ' + e.task, e.duration * 1000); });

View File

@ -2,6 +2,21 @@ var fs = require('fs');
var path = require('path');
module.exports = function(gulp, plugins, config) {
function symlink(relativeFolder, linkDir) {
var sourceDir = path.join('..', relativeFolder);
if (!fs.existsSync(linkDir)) {
console.log('creating link', linkDir, sourceDir);
try {
fs.symlinkSync(sourceDir, linkDir, 'dir');
}
catch(e) {
var sourceDir = path.join(config.dir, relativeFolder);
console.log('linking failed: trying to hard copy', linkDir, sourceDir);
copyRecursiveSync(sourceDir, linkDir);
}
}
}
return function() {
var nodeModulesDir = path.join(config.dir, 'node_modules');
if (!fs.existsSync(nodeModulesDir)) {
@ -11,20 +26,12 @@ module.exports = function(gulp, plugins, config) {
if (relativeFolder === 'node_modules') {
return;
}
var sourceDir = path.join('..', relativeFolder);
var linkDir = path.join(nodeModulesDir, relativeFolder);
if (!fs.existsSync(linkDir)) {
console.log('creating link', linkDir, sourceDir);
try {
fs.symlinkSync(sourceDir, linkDir, 'dir');
}
catch(e) {
var sourceDir = path.join(config.dir, relativeFolder);
console.log('linking failed: trying to hard copy', linkDir, sourceDir);
copyRecursiveSync(sourceDir, linkDir);
}
}
symlink(relativeFolder, linkDir);
});
// Also symlink tools we release independently to NPM, so tests can require metadata, etc.
symlink('../../tools/metadata', path.join(nodeModulesDir, 'ts-metadata-collector'));
};
};

View File

@ -0,0 +1,72 @@
# Angular Template Compiler
Angular applications are built with templates, which may be `.html` or `.css` files,
or may be inline `template` attributes on Decorators like `@Component`.
These templates are compiled into executable JS at application runtime (except in `interpretation` mode).
This compilation can occur on the client, but it results in slower bootstrap time, and also
requires that the compiler be included in the code downloaded to the client.
You can produce smaller, faster applications by running Angular's compiler as a build step,
and then downloading only the executable JS to the client.
## Configuration
The `tsconfig.json` file is expected to contain an additional configuration block:
```
"angularCompilerOptions": {
"genDir": "."
}
```
the `genDir` option controls the path (relative to `tsconfig.json`) where the generated file tree
will be written. More options may be added as we implement more features.
We recommend you avoid checking generated files into version control. This permits a state where
the generated files in the repository were created from sources that were never checked in,
making it impossible to reproduce the current state. Also, your changes will effectively appear
twice in code reviews, with the generated version inscrutible by the reviewer.
In TypeScript 1.8, the generated sources will have to be written alongside your originals,
so set `genDir` to the same location as your files (typicially the same as `rootDir`).
Add `**/*.ngfactory.ts` to your `.gitignore` or other mechanism for your version control system.
In TypeScript 1.9 and above, you can add a generated folder into your application,
such as `codegen`. Using the `rootDirs` option, you can allow relative imports like
`import {} from './foo.ngfactory'` even though the `src` and `codegen` trees are distinct.
Add `**/codegen` to your `.gitignore` or similar.
Note that in the second option, TypeScript will emit the code into two parallel directories
as well. This is by design, see https://github.com/Microsoft/TypeScript/issues/8245.
This makes the configuration of your runtime module loader more complex, so we don't recommend
this option yet.
See the example in the `test/` directory for a working example.
## Compiler CLI
This program mimics the TypeScript tsc command line. It accepts a `-p` flag which points to a
`tsconfig.json` file, or a directory containing one.
This CLI is intended for demos, prototyping, or for users with simple build systems
that run bare `tsc`.
Users with a build system should expect an Angular 2 template plugin. Such a plugin would be
based on the `index.ts` in this directory, but should share the TypeScript compiler instance
with the one already used in the plugin for TypeScript typechecking and emit.
## Design
At a high level, this program
- collects static metadata about the sources using the `ts-metadata-collector` package in angular2
- uses the `OfflineCompiler` from `angular2/src/compiler/compiler` to codegen additional `.ts` files
- these `.ts` files are written to the `genDir` path, then compiled together with the application.
## For developers
Run the compiler from source:
```
# Build angular2
gulp build.js.cjs
# Build the compiler
./node_modules/.bin/tsc -p tools/compiler_cli/src
# Run it on the test project
node ./dist/js/cjs/compiler_cli -p tools/compiler_cli/test
```

View File

@ -0,0 +1,142 @@
/**
* Transform template html and css into executable code.
* Intended to be used in a build step.
*/
import * as ts from 'typescript';
import * as path from 'path';
import * as compiler from 'angular2/compiler';
import {StaticReflector} from 'angular2/src/compiler/static_reflector';
import {CompileMetadataResolver} from 'angular2/src/compiler/metadata_resolver';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {DirectiveNormalizer} from 'angular2/src/compiler/directive_normalizer';
import {Lexer} from 'angular2/src/compiler/expression_parser/lexer';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {TemplateParser} from 'angular2/src/compiler/template_parser';
import {DomElementSchemaRegistry} from 'angular2/src/compiler/schema/dom_element_schema_registry';
import {StyleCompiler} from 'angular2/src/compiler/style_compiler';
import {ViewCompiler} from 'angular2/src/compiler/view_compiler/view_compiler';
import {TypeScriptEmitter} from 'angular2/src/compiler/output/ts_emitter';
import {RouterLinkTransform} from 'angular2/src/router/directives/router_link_transform';
import {Parse5DomAdapter} from 'angular2/platform/server';
import {MetadataCollector} from 'ts-metadata-collector';
import {NodeReflectorHost} from './reflector_host';
import {wrapCompilerHost, CodeGeneratorHost} from './compiler_host';
const SOURCE_EXTENSION = /\.[jt]s$/;
const PREAMBLE = `/**
* This file is generated by the Angular 2 template compiler.
* Do not edit.
*/
`;
export interface AngularCompilerOptions {
// Absolute path to a directory where generated file structure is written
genDir: string;
}
export class CodeGenerator {
constructor(private ngOptions: AngularCompilerOptions, private basePath: string,
public program: ts.Program, public host: CodeGeneratorHost,
private staticReflector: StaticReflector, private resolver: CompileMetadataResolver,
private compiler: compiler.OfflineCompiler,
private reflectorHost: NodeReflectorHost) {}
private generateSource(metadatas: compiler.CompileDirectiveMetadata[]) {
const normalize = (metadata: compiler.CompileDirectiveMetadata) => {
const directiveType = metadata.type.runtime;
const directives = this.resolver.getViewDirectivesMetadata(directiveType);
const pipes = this.resolver.getViewPipesMetadata(directiveType);
return new compiler.NormalizedComponentWithViewDirectives(metadata, directives, pipes);
};
return this.compiler.compileTemplates(metadatas.map(normalize));
}
private readComponents(absSourcePath: string) {
const result: Promise<compiler.CompileDirectiveMetadata>[] = [];
const metadata = this.staticReflector.getModuleMetadata(absSourcePath);
if (!metadata) {
console.log(`WARNING: no metadata found for ${absSourcePath}`);
return result;
}
const symbols = Object.keys(metadata['metadata']);
if (!symbols || !symbols.length) {
return result;
}
for (const symbol of symbols) {
const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
let directive: compiler.CompileDirectiveMetadata;
directive = this.resolver.maybeGetDirectiveMetadata(<any>staticType);
if (!directive || !directive.isComponent) {
continue;
}
result.push(this.compiler.normalizeDirectiveMetadata(directive));
}
return result;
}
codegen() {
Parse5DomAdapter.makeCurrent();
const generateOneFile = (absSourcePath: string) =>
Promise.all(this.readComponents(absSourcePath))
.then((metadatas: compiler.CompileDirectiveMetadata[]) => {
if (!metadatas || !metadatas.length) {
return;
}
const generated = this.generateSource(metadatas);
const sourceFile = this.program.getSourceFile(absSourcePath);
// Write codegen in a directory structure matching the sources.
// TODO(alexeagle): maybe use generated.moduleUrl instead of hardcoded ".ngfactory.ts"
// TODO(alexeagle): relativize paths by the rootDirs option
const emitPath =
path.join(this.ngOptions.genDir, path.relative(this.basePath, absSourcePath))
.replace(SOURCE_EXTENSION, '.ngfactory.ts');
this.host.writeFile(emitPath, PREAMBLE + generated.source, false, () => {},
[sourceFile]);
})
.catch((e) => { console.error(e.stack); });
return Promise.all(this.program.getRootFileNames()
.filter(f => !/\.ngfactory\.ts$/.test(f))
.map(generateOneFile));
}
static create(ngOptions: AngularCompilerOptions, parsed: ts.ParsedCommandLine, basePath: string,
compilerHost: ts.CompilerHost):
{errors?: ts.Diagnostic[], generator?: CodeGenerator} {
const program = ts.createProgram(parsed.fileNames, parsed.options, compilerHost);
const errors = program.getOptionsDiagnostics();
if (errors && errors.length) {
return {errors};
}
const metadataCollector = new MetadataCollector();
const reflectorHost =
new NodeReflectorHost(program, metadataCollector, compilerHost, parsed.options);
const xhr: compiler.XHR = {get: (s: string) => Promise.resolve(compilerHost.readFile(s))};
const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
const staticReflector = new StaticReflector(reflectorHost);
const htmlParser = new HtmlParser();
const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser);
const parser = new Parser(new Lexer());
const tmplParser = new TemplateParser(parser, new DomElementSchemaRegistry(), htmlParser,
/*console*/ null, [new RouterLinkTransform(parser)]);
const offlineCompiler = new compiler.OfflineCompiler(
normalizer, tmplParser, new StyleCompiler(urlResolver),
new ViewCompiler(new compiler.CompilerConfig(true, true, true)), new TypeScriptEmitter());
const resolver = new CompileMetadataResolver(
new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector),
new compiler.ViewResolver(staticReflector), null, null, staticReflector);
return {
generator: new CodeGenerator(ngOptions, basePath, program,
wrapCompilerHost(compilerHost, parsed.options), staticReflector,
resolver, offlineCompiler, reflectorHost)
};
}
}

View File

@ -0,0 +1,63 @@
import * as ts from 'typescript';
import * as path from 'path';
import {convertDecorators} from 'tsickle';
const DEBUG = false;
function debug(msg: string, ...o: any[]) {
if (DEBUG) console.log(msg, ...o);
}
/**
* Implementation of CompilerHost that forwards all methods to another instance.
* Useful for partial implementations to override only methods they care about.
*/
export abstract class DelegatingHost implements ts.CompilerHost {
constructor(protected delegate: ts.CompilerHost) {}
getSourceFile =
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) =>
this.delegate.getSourceFile(fileName, languageVersion, onError);
getCancellationToken = () => this.delegate.getCancellationToken();
getDefaultLibFileName = (options: ts.CompilerOptions) =>
this.delegate.getDefaultLibFileName(options);
getDefaultLibLocation = () => this.delegate.getDefaultLibLocation();
writeFile: ts.WriteFileCallback = this.delegate.writeFile;
getCurrentDirectory = () => this.delegate.getCurrentDirectory();
getCanonicalFileName = (fileName: string) => this.delegate.getCanonicalFileName(fileName);
useCaseSensitiveFileNames = () => this.delegate.useCaseSensitiveFileNames();
getNewLine = () => this.delegate.getNewLine();
fileExists = (fileName: string) => this.delegate.fileExists(fileName);
readFile = (fileName: string) => this.delegate.readFile(fileName);
trace = (s: string) => this.delegate.trace(s);
directoryExists = (directoryName: string) => this.delegate.directoryExists(directoryName);
}
const TSICKLE_SUPPORT = `interface DecoratorInvocation {
type: Function;
args?: any[];
}
`;
export class CodeGeneratorHost extends DelegatingHost {
// Additional diagnostics gathered by pre- and post-emit transformations.
public diagnostics: ts.Diagnostic[] = [];
constructor(delegate: ts.CompilerHost, private options: ts.CompilerOptions) { super(delegate); }
getSourceFile =
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => {
const originalContent = this.delegate.readFile(fileName);
let newContent = originalContent;
if (!/\.d\.ts$/.test(fileName)) {
const converted = convertDecorators(fileName, originalContent);
if (converted.diagnostics) {
this.diagnostics.push(...converted.diagnostics);
}
newContent = TSICKLE_SUPPORT + converted.output;
debug(newContent);
}
return ts.createSourceFile(fileName, newContent, languageVersion, true);
}
}
export function wrapCompilerHost(delegate: ts.CompilerHost,
options: ts.CompilerOptions): CodeGeneratorHost {
return new CodeGeneratorHost(delegate, options);
}

View File

@ -0,0 +1,55 @@
// TODO(alexeagle): use --lib=node when available; remove this reference
// https://github.com/Microsoft/TypeScript/pull/7757#issuecomment-205644657
/// <reference path="../../typings/node/node.d.ts"/>
// Must be imported first, because angular2 decorators throws on load.
import 'reflect-metadata';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {tsc, check} from './tsc';
import {CodeGenerator} from './codegen';
const DEBUG = false;
function debug(msg: string, ...o: any[]) {
if (DEBUG) console.log(msg, ...o);
}
export function main(project: string, basePath?: string): Promise<number> {
// file names in tsconfig are resolved relative to this absolute path
basePath = path.join(process.cwd(), basePath || project);
// read the configuration options from wherever you store them
const {parsed, ngOptions} = tsc.readConfiguration(project, basePath);
const host = ts.createCompilerHost(parsed.options, true);
const {errors, generator} = CodeGenerator.create(ngOptions, parsed, basePath, host);
check(errors);
return generator.codegen()
// use our compiler host, which wraps the built-in one from TypeScript
// This allows us to add features like --stripDesignTimeDecorators to optimize your
// application more.
.then(() => tsc.typeCheckAndEmit(generator.host, generator.program))
.catch(rejected => {
console.error('Compile failed\n', rejected.message);
throw new Error(rejected);
});
}
// CLI entry point
if (require.main === module) {
const args = require('minimist')(process.argv.slice(2));
try {
main(args.p || args.project || '.', args.basePath)
.then(exitCode => process.exit(exitCode))
.catch(r => { process.exit(1); });
} catch (e) {
console.error(e.stack);
console.error("Compilation failed");
process.exit(1);
}
}

View File

@ -0,0 +1,147 @@
import {StaticReflectorHost, StaticSymbol} from 'angular2/src/compiler/static_reflector';
import * as ts from 'typescript';
import {MetadataCollector, ModuleMetadata} from 'ts-metadata-collector';
import * as fs from 'fs';
import * as path from 'path';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
export class NodeReflectorHost implements StaticReflectorHost {
constructor(private program: ts.Program, private metadataCollector: MetadataCollector,
private compilerHost: ts.CompilerHost, private options: ts.CompilerOptions) {}
private resolve(m: string, containingFile: string) {
const resolved =
ts.resolveModuleName(m, containingFile, this.options, this.compilerHost).resolvedModule;
return resolved ? resolved.resolvedFileName : null
};
/**
* We want a moduleId that will appear in import statements in the generated code.
* These need to be in a form that system.js can load, so absolute file paths don't work.
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
* they are resolvable by the moduleResolution strategy from the CompilerHost.
*/
private getModuleId(declarationFile: string, containingFile: string) {
const parts = declarationFile.replace(EXT, '').split(path.sep);
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.resolve(candidate, containingFile) === declarationFile) {
let pkg = parts[index];
let pkgPath = parts.slice(index + 1, parts.length).join(path.sep);
return `asset:${pkg}/lib/${pkgPath}`;
}
}
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.resolve('.' + path.sep + candidate, containingFile) === declarationFile) {
return `asset:./lib/${candidate}`;
}
}
throw new Error(
`Unable to find any resolvable import for ${declarationFile} relative to ${containingFile}`);
}
findDeclaration(module: string, symbolName: string, containingFile: string,
containingModule?: string): StaticSymbol {
if (!containingFile || !containingFile.length) {
if (module.indexOf(".") === 0) {
throw new Error("Resolution of relative paths requires a containing file.");
}
// Any containing file gives the same result for absolute imports
containingFile = 'index.ts';
}
try {
const filePath = this.resolve(module, containingFile);
if (!filePath) {
throw new Error(`Could not resolve module ${module} relative to ${containingFile}`);
}
const tc = this.program.getTypeChecker();
const sf = this.program.getSourceFile(filePath);
let symbol = tc.getExportsOfModule((<any>sf).symbol).find(m => m.name === symbolName);
if (!symbol) {
throw new Error(`can't find symbol ${symbolName} exported from module ${filePath}`);
}
while (symbol &&
symbol.flags & ts.SymbolFlags.Alias) { // This is an alias, follow what it aliases
symbol = tc.getAliasedSymbol(symbol);
}
const declaration = symbol.getDeclarations()[0];
const declarationFile = declaration.getSourceFile().fileName;
const moduleId = this.getModuleId(declarationFile, containingFile);
return this.getStaticSymbol(moduleId, declarationFile, symbol.getName());
} catch (e) {
console.error(`can't resolve module ${module} from ${containingFile}`);
throw e;
}
}
private typeCache = new Map<string, StaticSymbol>();
/**
* getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded.
* All types passed to the StaticResolver should be pseudo-types returned by this method.
*
* @param moduleId the module identifier as an absolute path.
* @param declarationFile the absolute path of the file where the symbol is declared
* @param name the name of the type.
*/
getStaticSymbol(moduleId: string, declarationFile: string, name: string): StaticSymbol {
let key = `"${declarationFile}".${name}`;
let result = this.typeCache.get(key);
if (!result) {
result = new StaticSymbol(moduleId, declarationFile, name);
this.typeCache.set(key, result);
}
return result;
}
// TODO(alexeagle): take a statictype
getMetadataFor(filePath: string): ModuleMetadata {
if (!fs.existsSync(filePath)) {
throw new Error(`No such file '${filePath}'`);
}
if (DTS.test(filePath)) {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (fs.existsSync(metadataPath)) {
return this.readMetadata(metadataPath);
}
}
let sf = this.program.getSourceFile(filePath);
if (!sf) {
throw new Error(`Source file ${filePath} not present in program.`);
}
const metadata = this.metadataCollector.getMetadata(sf, this.program.getTypeChecker());
return metadata;
}
readMetadata(filePath: string) {
try {
const result = JSON.parse(fs.readFileSync(filePath, {encoding: 'utf-8'}));
return result;
} catch (e) {
console.error(`Failed to read JSON file ${filePath}`);
throw e;
}
}
writeMetadata(emitFilePath: string, sourceFile: ts.SourceFile) {
if (DTS.test(emitFilePath)) {
const path = emitFilePath.replace(DTS, '.metadata.json');
const metadata =
this.metadataCollector.getMetadata(sourceFile, this.program.getTypeChecker());
if (metadata && metadata.metadata) {
const metadataText = JSON.stringify(metadata);
fs.writeFileSync(path, metadataText, {encoding: 'utf-8'});
}
}
}
}

View File

@ -0,0 +1,98 @@
import * as ts from 'typescript';
// Don't import from fs in general, that's the CompilerHost's job
import {lstatSync} from 'fs';
import * as path from 'path';
import {AngularCompilerOptions} from './codegen';
import {CodeGeneratorHost} from './compiler_host';
/**
* Our interface to the TypeScript standard compiler.
* If you write an Angular compiler plugin for another build tool,
* you should implement a similar interface.
*/
export interface CompilerInterface {
readConfiguration(
project: string,
basePath: string): {parsed: ts.ParsedCommandLine, ngOptions: AngularCompilerOptions};
typeCheckAndEmit(compilerHost: CodeGeneratorHost, oldProgram?: ts.Program): number;
}
const DEBUG = false;
const SOURCE_EXTENSION = /\.[jt]s$/;
function debug(msg: string, ...o: any[]) {
if (DEBUG) console.log(msg, ...o);
}
export function formatDiagnostics(diags: ts.Diagnostic[]): string {
return diags.map((d) => {
let res = ts.DiagnosticCategory[d.category];
if (d.file) {
res += ' at ' + d.file.fileName + ':';
const {line, character} = d.file.getLineAndCharacterOfPosition(d.start);
res += (line + 1) + ':' + (character + 1) + ':';
}
res += ' ' + ts.flattenDiagnosticMessageText(d.messageText, '\n');
return res;
})
.join('\n');
}
export function check(diags: ts.Diagnostic[]) {
if (diags && diags.length && diags[0]) {
throw new Error(formatDiagnostics(diags));
}
}
export class Tsc implements CompilerInterface {
public ngOptions: AngularCompilerOptions;
public parsed: ts.ParsedCommandLine;
private basePath: string;
readConfiguration(project: string, basePath: string) {
this.basePath = basePath;
// Allow a directory containing tsconfig.json as the project value
if (lstatSync(project).isDirectory()) {
project = path.join(project, "tsconfig.json");
}
const {config, error} = ts.readConfigFile(project, ts.sys.readFile);
check([error]);
this.parsed =
ts.parseJsonConfigFileContent(config, {readDirectory: ts.sys.readDirectory}, basePath);
check(this.parsed.errors);
// Default codegen goes to the current directory
// Parsed options are already converted to absolute paths
this.ngOptions = config.angularCompilerOptions || {};
this.ngOptions.genDir = path.join(basePath, this.ngOptions.genDir || '.');
return {parsed: this.parsed, ngOptions: this.ngOptions};
}
typeCheckAndEmit(compilerHost: CodeGeneratorHost, oldProgram?: ts.Program): number {
const program =
ts.createProgram(this.parsed.fileNames, this.parsed.options, compilerHost, oldProgram);
debug("Checking global diagnostics...");
check(program.getGlobalDiagnostics());
debug("Type checking...");
{
let diagnostics: ts.Diagnostic[] = [];
for (let sf of program.getSourceFiles()) {
diagnostics.push(...ts.getPreEmitDiagnostics(program, sf));
}
check(diagnostics);
}
debug("Emitting outputs...");
const {diagnostics, emitSkipped} = program.emit();
check(diagnostics);
check(compilerHost.diagnostics);
return emitSkipped ? 1 : 0;
}
}
export var tsc: CompilerInterface = new Tsc();

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": ["es6", "dom"],
"noImplicitAny": true,
"sourceMap": true,
"baseUrl": "../../..",
"paths": {
"angular2/*": ["dist/js/cjs/angular2/*"],
"ts-metadata-collector": ["dist/tools/metadata"]
},
"experimentalDecorators": true,
"rootDir": ".",
// Write to a directory that has the node_modules symlink in a parent
"outDir": "../../../dist/js/cjs/compiler_cli",
"declaration": true
},
"exclude": ["test"]
}

View File

@ -0,0 +1 @@
<div></div>

View File

@ -0,0 +1,21 @@
import {Component} from 'angular2/core';
@Component({
selector: 'my-comp',
template: '<div></div>',
})
export class MyComp {
}
@Component({
selector: 'next-comp',
templateUrl: './multiple_components.html',
})
export class NextComp {
}
// Verify that exceptions from DirectiveResolver don't propagate
export function NotADirective(c: any): void {}
@NotADirective
export class HasCustomDecorator {
}

View File

@ -0,0 +1,2 @@
<div>{{ctxProp}}</div>
<form><input type="button" [(ngModel)]="ctxProp"/></form>

View File

@ -0,0 +1,14 @@
import {Component, Injectable} from 'angular2/core';
import {FORM_DIRECTIVES} from 'angular2/common';
import {MyComp} from './a/multiple_components';
@Component({
selector: 'basic',
templateUrl: './basic.html',
directives: [MyComp, FORM_DIRECTIVES],
})
@Injectable()
export class Basic {
ctxProp: string;
constructor() { this.ctxProp = 'initial value'; }
}

View File

@ -0,0 +1,8 @@
import {coreBootstrap, ReflectiveInjector} from 'angular2/core';
import {browserPlatform, BROWSER_APP_PROVIDERS} from 'angular2/platform/browser';
import {BasicNgFactory} from './basic.ngfactory';
import {Basic} from './basic';
const appInjector =
ReflectiveInjector.resolveAndCreate(BROWSER_APP_PROVIDERS, browserPlatform().injector);
coreBootstrap(appInjector, BasicNgFactory);

2
tools/compiler_cli/test/src/dep.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// Verify we don't try to extract metadata for .d.ts files
export declare var a: string;

View File

@ -0,0 +1,27 @@
{
"angularCompilerOptions": {
// For TypeScript 1.8, we have to lay out generated files
// in the same source directory with your code.
"genDir": "."
},
"compilerOptions": {
"target": "es6",
"experimentalDecorators": true,
"noImplicitAny": false,
"moduleResolution": "node",
"outDir": "../../../dist/tools/compiler_cli/test/built",
"rootDir": "src",
/**
* These options are only needed because the test depends
* on locally-built sources, not NPM distributions.
*/
"baseUrl": "../../..",
"paths": {
"angular2/*": ["dist/js/cjs/angular2/*"],
"rxjs/*": ["node_modules/rxjs/*"],
"ts-metadata-collector": ["dist/tools/metadata"]
}
}
}