feat(offline compiler): a replacement for tsc that compiles templates
see #7483.
This commit is contained in:
parent
33e53c9a59
commit
78946fe9fa
|
@ -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
|
||||
|
|
60
gulpfile.js
60
gulpfile.js
|
@ -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); });
|
||||
|
|
|
@ -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'));
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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"]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<div></div>
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<div>{{ctxProp}}</div>
|
||||
<form><input type="button" [(ngModel)]="ctxProp"/></form>
|
|
@ -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'; }
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1,2 @@
|
|||
// Verify we don't try to extract metadata for .d.ts files
|
||||
export declare var a: string;
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue