From 4c263979375a72dc4a3b95673b8a26b50c0efc79 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Tue, 24 May 2016 10:53:48 -0700 Subject: [PATCH] chore(ngc): refactor out tsc-wrapped This allows angular's build to depend on some extensions, but not on code generation, and breaks a cycle in the angular build We now merge ts-metadata-collector into tsc-wrapped and stop publishing the former. --- build.sh | 18 ++---- modules/@angular/compiler_cli/README.md | 2 +- modules/@angular/compiler_cli/index.ts | 2 +- modules/@angular/compiler_cli/package.json | 3 +- modules/@angular/compiler_cli/src/codegen.ts | 38 +++-------- modules/@angular/compiler_cli/src/main.ts | 63 ++---------------- .../compiler_cli/src/reflector_host.ts | 47 ++++---------- .../@angular/compiler_cli/tsconfig-es5.json | 2 +- modules/tsconfig.json | 2 +- scripts/ci-lite/build.sh | 8 +-- tools/broccoli/broccoli-typescript.ts | 2 +- tools/build/linknodemodules.js | 2 +- tools/ts-metadata-collector/README.md | 15 ----- tools/ts-metadata-collector/index.ts | 2 - tools/tsc-watch/index.ts | 3 +- tools/tsc-wrapped/README.md | 30 +++++++++ tools/tsc-wrapped/index.ts | 5 ++ .../package.json | 15 +++-- .../src/collector.ts | 0 .../tsc-wrapped}/src/compiler_host.ts | 33 ++++++---- .../src/evaluator.ts | 0 tools/tsc-wrapped/src/main.ts | 64 +++++++++++++++++++ tools/tsc-wrapped/src/options.ts | 20 ++++++ .../src/schema.ts | 0 .../src/symbols.ts | 0 .../tsc-wrapped}/src/tsc.ts | 19 ++++-- .../test/collector.spec.ts | 0 .../test/evaluator.spec.ts | 0 .../test/symbols.spec.ts | 0 tools/tsc-wrapped/test/tsc.spec.ts | 28 ++++++++ .../test/typescript.mocks.ts | 0 31 files changed, 230 insertions(+), 193 deletions(-) delete mode 100644 tools/ts-metadata-collector/README.md delete mode 100644 tools/ts-metadata-collector/index.ts create mode 100644 tools/tsc-wrapped/README.md create mode 100644 tools/tsc-wrapped/index.ts rename tools/{ts-metadata-collector => tsc-wrapped}/package.json (55%) rename tools/{ts-metadata-collector => tsc-wrapped}/src/collector.ts (100%) rename {modules/@angular/compiler_cli => tools/tsc-wrapped}/src/compiler_host.ts (78%) rename tools/{ts-metadata-collector => tsc-wrapped}/src/evaluator.ts (100%) create mode 100644 tools/tsc-wrapped/src/main.ts create mode 100644 tools/tsc-wrapped/src/options.ts rename tools/{ts-metadata-collector => tsc-wrapped}/src/schema.ts (100%) rename tools/{ts-metadata-collector => tsc-wrapped}/src/symbols.ts (100%) rename {modules/@angular/compiler_cli => tools/tsc-wrapped}/src/tsc.ts (85%) rename tools/{ts-metadata-collector => tsc-wrapped}/test/collector.spec.ts (100%) rename tools/{ts-metadata-collector => tsc-wrapped}/test/evaluator.spec.ts (100%) rename tools/{ts-metadata-collector => tsc-wrapped}/test/symbols.spec.ts (100%) create mode 100644 tools/tsc-wrapped/test/tsc.spec.ts rename tools/{ts-metadata-collector => tsc-wrapped}/test/typescript.mocks.ts (100%) diff --git a/build.sh b/build.sh index 6124e07667..b3c83d16c7 100755 --- a/build.sh +++ b/build.sh @@ -35,14 +35,8 @@ cd - TSCONFIG=./modules/tsconfig.json echo "====== (all)COMPILING: \$(npm bin)/tsc -p ${TSCONFIG} =====" # compile ts code -# TODO: Right now we have a cycle in that the compiler_cli depends on Angular -# but we need it to compile Angular. -# The solution right now is to do 2 compilation runs. -# Fix this by separating the metadata extraction into a separate binary that does -# not depend on Angular. -$(npm bin)/tsc -p ${TSCONFIG} -NG_TC="node dist/all/@angular/compiler_cli/src/main" -$NG_TC -p modules/tsconfig.json +TSC="node dist/tools/tsc-wrapped/src/main" +$TSC -p modules/tsconfig.json rm -rf ./dist/packages-dist @@ -68,8 +62,8 @@ do echo "====== COMPILING: \$(npm bin)/tsc -p ${SRCDIR}/tsconfig-es5.json =====" $(npm bin)/tsc -p ${SRCDIR}/tsconfig-es5.json else - echo "====== COMPILING: ${NG_TC} -p ${SRCDIR}/tsconfig-es5.json =====" - $NG_TC -p ${SRCDIR}/tsconfig-es5.json + echo "====== COMPILING: ${TSC} -p ${SRCDIR}/tsconfig-es5.json =====" + $TSC -p ${SRCDIR}/tsconfig-es5.json fi cp ${SRCDIR}/package.json ${DESTDIR}/ @@ -91,8 +85,8 @@ do echo "====== (esm)COMPILING: \$(npm bin)/tsc -p ${SRCDIR}/tsconfig-es2015.json =====" $(npm bin)/tsc -p ${SRCDIR}/tsconfig-es2015.json else - echo "====== (esm)COMPILING: $NG_TC -p ${SRCDIR}/tsconfig-es2015.json =====" - $NG_TC -p ${SRCDIR}/tsconfig-es2015.json + echo "====== (esm)COMPILING: $TSC -p ${SRCDIR}/tsconfig-es2015.json =====" + $TSC -p ${SRCDIR}/tsconfig-es2015.json fi echo "====== BUNDLING: ${SRCDIR} =====" diff --git a/modules/@angular/compiler_cli/README.md b/modules/@angular/compiler_cli/README.md index b657986cc5..13ba227053 100644 --- a/modules/@angular/compiler_cli/README.md +++ b/modules/@angular/compiler_cli/README.md @@ -73,7 +73,7 @@ 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 +- collects static metadata about the sources using the `tsc-wrapped` 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. diff --git a/modules/@angular/compiler_cli/index.ts b/modules/@angular/compiler_cli/index.ts index 8bb7748eca..41f297e2bd 100644 --- a/modules/@angular/compiler_cli/index.ts +++ b/modules/@angular/compiler_cli/index.ts @@ -1,3 +1,3 @@ export {CodeGenerator} from './src/codegen'; export {NodeReflectorHost} from './src/reflector_host'; -export {TsickleHost, MetadataWriterHost} from './src/compiler_host'; +export * from 'tsc-wrapped'; diff --git a/modules/@angular/compiler_cli/package.json b/modules/@angular/compiler_cli/package.json index 41c40482d1..950d1c5b2f 100644 --- a/modules/@angular/compiler_cli/package.json +++ b/modules/@angular/compiler_cli/package.json @@ -8,8 +8,7 @@ "ngc": "./src/main.js" }, "dependencies": { - "ts-metadata-collector": "^0.1.0", - "tsickle": "^0.1.2", + "tsc-wrapped": "^0.1.0", "reflect-metadata": "^0.1.2", "parse5": "1.3.2" }, diff --git a/modules/@angular/compiler_cli/src/codegen.ts b/modules/@angular/compiler_cli/src/codegen.ts index f5805f1e7f..1eaac18236 100644 --- a/modules/@angular/compiler_cli/src/codegen.ts +++ b/modules/@angular/compiler_cli/src/codegen.ts @@ -4,6 +4,7 @@ */ import * as ts from 'typescript'; import * as path from 'path'; +import {AngularCompilerOptions} from 'tsc-wrapped'; import * as compiler from '@angular/compiler'; import {ViewEncapsulation} from '@angular/core'; @@ -23,7 +24,6 @@ import { import {Parse5DomAdapter} from '@angular/platform-server'; -import {MetadataCollector} from 'ts-metadata-collector'; import {NodeReflectorHost} from './reflector_host'; import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities'; @@ -35,30 +35,8 @@ const PREAMBLE = `/** */ `; -// TODO(alexeagle): we end up passing options and ngOptions everywhere. -// Maybe this should extend ts.CompilerOptions so we only need this one. -export interface AngularCompilerOptions { - // Absolute path to a directory where generated file structure is written - genDir: string; - - // Path to the directory containing the tsconfig.json file. - basePath: string; - - // Don't do the template code generation - skipTemplateCodegen: boolean; - - // Don't produce .metadata.json files (they don't work for bundled emit with --out) - skipMetadataEmit: boolean; - - // Lookup angular's symbols using the old angular2/... npm namespace. - legacyPackageLayout: boolean; - - // Print extra information while running the compiler - trace: boolean; -} - export class CodeGenerator { - constructor(private options: ts.CompilerOptions, private ngOptions: AngularCompilerOptions, + constructor(private options: AngularCompilerOptions, private program: ts.Program, public host: ts.CompilerHost, private staticReflector: StaticReflector, private resolver: CompileMetadataResolver, private compiler: compiler.OfflineCompiler, @@ -107,9 +85,9 @@ export class CodeGenerator { // Write codegen in a directory structure matching the sources. private calculateEmitPath(filePath: string) { - let root = this.ngOptions.basePath; + let root = this.options.basePath; for (let eachRootDir of this.options.rootDirs || []) { - if (this.ngOptions.trace) { + if (this.options.trace) { console.log(`Check if ${filePath} is under rootDirs element ${eachRootDir}`); } if (path.relative(eachRootDir, filePath).indexOf('.') !== 0) { @@ -117,7 +95,7 @@ export class CodeGenerator { } } - return path.join(this.ngOptions.genDir, path.relative(root, filePath)); + return path.join(this.options.genDir, path.relative(root, filePath)); } // TODO(tbosch): add a cache for shared css files @@ -170,11 +148,11 @@ export class CodeGenerator { return Promise.all(stylesheetPromises.concat(compPromises)); } - static create(ngOptions: AngularCompilerOptions, program: ts.Program, options: ts.CompilerOptions, + static create(options: AngularCompilerOptions, program: ts.Program, compilerHost: ts.CompilerHost): CodeGenerator { const xhr: compiler.XHR = {get: (s: string) => Promise.resolve(compilerHost.readFile(s))}; const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver(); - const reflectorHost = new NodeReflectorHost(program, compilerHost, options, ngOptions); + const reflectorHost = new NodeReflectorHost(program, compilerHost, options); const staticReflector = new StaticReflector(reflectorHost); StaticAndDynamicReflectionCapabilities.install(staticReflector); const htmlParser = new HtmlParser(); @@ -190,7 +168,7 @@ export class CodeGenerator { new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), new compiler.ViewResolver(staticReflector), null, null, staticReflector); - return new CodeGenerator(options, ngOptions, program, compilerHost, staticReflector, resolver, + return new CodeGenerator(options, program, compilerHost, staticReflector, resolver, offlineCompiler, reflectorHost); } } diff --git a/modules/@angular/compiler_cli/src/main.ts b/modules/@angular/compiler_cli/src/main.ts index cc33e93363..edcff92032 100644 --- a/modules/@angular/compiler_cli/src/main.ts +++ b/modules/@angular/compiler_cli/src/main.ts @@ -3,72 +3,19 @@ // 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 {MetadataWriterHost, TsickleHost} from './compiler_host'; -import {NodeReflectorHost} from './reflector_host'; +import * as tsc from 'tsc-wrapped'; + import {CodeGenerator} from './codegen'; -import {MetadataCollector, ModuleMetadata} from 'ts-metadata-collector'; -const DEBUG = false; - -function debug(msg: string, ...o: any[]) { - if (DEBUG) console.log(msg, ...o); -} - -export function main(project: string, basePath?: string): Promise { - try { - let projectDir = project; - if (fs.lstatSync(project).isFile()) { - projectDir = path.dirname(project); - } - // file names in tsconfig are resolved relative to this absolute path - basePath = path.join(process.cwd(), basePath || projectDir); - - // read the configuration options from wherever you store them - const {parsed, ngOptions} = tsc.readConfiguration(project, basePath); - ngOptions.basePath = basePath; - - const host = ts.createCompilerHost(parsed.options, true); - - let codegenStep: Promise; - - const program = ts.createProgram(parsed.fileNames, parsed.options, host); - const errors = program.getOptionsDiagnostics(); - check(errors); - - const doCodegen = ngOptions.skipTemplateCodegen ? - Promise.resolve(null) : - CodeGenerator.create(ngOptions, program, parsed.options, host).codegen(); - - return doCodegen.then(() => { - tsc.typeCheck(host, program); - - // Emit *.js with Decorators lowered to Annotations, and also *.js.map - const tsicklePreProcessor = new TsickleHost(host, parsed.options); - tsc.emit(tsicklePreProcessor, program); - - if (!ngOptions.skipMetadataEmit) { - // Emit *.metadata.json and *.d.ts - // Not in the same emit pass with above, because tsickle erases - // decorators which we want to read or document. - // Do this emit second since TypeScript will create missing directories for us - // in the standard emit. - const metadataWriter = new MetadataWriterHost(host, program, parsed.options, ngOptions); - tsc.emit(metadataWriter, program); - } - }); - } catch (e) { - return Promise.reject(e); - } +function codegen(ngOptions: tsc.AngularCompilerOptions, program: ts.Program, host: ts.CompilerHost) { + return CodeGenerator.create(ngOptions, program, host).codegen(); } // CLI entry point if (require.main === module) { const args = require('minimist')(process.argv.slice(2)); - main(args.p || args.project || '.', args.basePath) + tsc.main(args.p || args.project || '.', args.basePath, codegen) .then(exitCode => process.exit(exitCode)) .catch(e => { console.error(e.stack); diff --git a/modules/@angular/compiler_cli/src/reflector_host.ts b/modules/@angular/compiler_cli/src/reflector_host.ts index bceb9a3d2c..75d1983d87 100644 --- a/modules/@angular/compiler_cli/src/reflector_host.ts +++ b/modules/@angular/compiler_cli/src/reflector_host.ts @@ -1,10 +1,10 @@ import {StaticReflectorHost, StaticSymbol} from './static_reflector'; import * as ts from 'typescript'; -import {MetadataCollector, ModuleMetadata} from 'ts-metadata-collector'; +import {AngularCompilerOptions, MetadataCollector, ModuleMetadata} from 'tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; -import {AngularCompilerOptions} from './codegen'; -import {ImportGenerator, AssetUrl} from './compiler_private'; +import {ImportGenerator, AssetUrl} from './compiler_private' + const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const DTS = /\.d\.ts$/; @@ -12,24 +12,15 @@ const DTS = /\.d\.ts$/; export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator { private metadataCollector = new MetadataCollector(); constructor(private program: ts.Program, private compilerHost: ts.CompilerHost, - private options: ts.CompilerOptions, private ngOptions: AngularCompilerOptions) {} + private options: AngularCompilerOptions) {} angularImportLocations() { - if (this.ngOptions.legacyPackageLayout) { - return { - coreDecorators: 'angular2/src/core/metadata', - diDecorators: 'angular2/src/core/di/decorators', - diMetadata: 'angular2/src/core/di/metadata', - provider: 'angular2/src/core/di/provider' - }; - } else { - return { - coreDecorators: '@angular/core/src/metadata', - diDecorators: '@angular/core/src/di/decorators', - diMetadata: '@angular/core/src/di/metadata', - provider: '@angular/core/src/di/provider' - }; - } + return { + coreDecorators: '@angular/core/src/metadata', + diDecorators: '@angular/core/src/di/decorators', + diMetadata: '@angular/core/src/di/metadata', + provider: '@angular/core/src/di/provider' + }; } private resolve(m: string, containingFile: string) { const resolved = @@ -63,7 +54,7 @@ export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator { // TODO(tbosch): if a file does not yet exist (because we compile it later), // we still need to create it so that the `resolve` method works! if (!this.compilerHost.fileExists(importedFile)) { - if (this.ngOptions.trace) { + if (this.options.trace) { console.log(`Generating empty file ${importedFile} to allow resolution of import`); } this.compilerHost.writeFile(importedFile, '', false); @@ -92,7 +83,7 @@ export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator { throw new Error("Resolution of relative paths requires a containing file."); } // Any containing file gives the same result for absolute imports - containingFile = path.join(this.ngOptions.basePath, 'index.ts'); + containingFile = path.join(this.options.basePath, 'index.ts'); } try { @@ -175,18 +166,4 @@ export class NodeReflectorHost implements StaticReflectorHost, ImportGenerator { throw e; } } - - writeMetadata(emitFilePath: string, sourceFile: ts.SourceFile) { - // TODO: replace with DTS filePath when https://github.com/Microsoft/TypeScript/pull/8412 is - // released - if (/*DTS*/ /\.js$/.test(emitFilePath)) { - const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.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'}); - } - } - } } diff --git a/modules/@angular/compiler_cli/tsconfig-es5.json b/modules/@angular/compiler_cli/tsconfig-es5.json index 096e147659..bcf04ae81c 100644 --- a/modules/@angular/compiler_cli/tsconfig-es5.json +++ b/modules/@angular/compiler_cli/tsconfig-es5.json @@ -15,7 +15,7 @@ "@angular/compiler": ["../../../dist/packages-dist/compiler"], "@angular/platform-server": ["../../../dist/packages-dist/platform-server"], "@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"], - "ts-metadata-collector": ["../../../dist/tools/ts-metadata-collector"] + "tsc-wrapped": ["../../../dist/tools/tsc-wrapped"] }, "experimentalDecorators": true, "rootDir": ".", diff --git a/modules/tsconfig.json b/modules/tsconfig.json index 689bc9ef17..27930f9a39 100644 --- a/modules/tsconfig.json +++ b/modules/tsconfig.json @@ -14,7 +14,7 @@ "selenium-webdriver": ["./@angular/typings/selenium-webdriver/selenium-webdriver.d.ts"], "rxjs/*": ["../node_modules/rxjs/*"], "@angular/*": ["./@angular/*"], - "ts-metadata-collector": ["../dist/tools/ts-metadata-collector"] + "tsc-wrapped": ["../dist/tools/tsc-wrapped"] }, "rootDir": ".", "inlineSourceMap": true, diff --git a/scripts/ci-lite/build.sh b/scripts/ci-lite/build.sh index 5791a90917..6583db4fab 100755 --- a/scripts/ci-lite/build.sh +++ b/scripts/ci-lite/build.sh @@ -11,13 +11,7 @@ cd ../.. $(npm bin)/tsc -p ./tools/tsconfig.json -# TODO: Right now we have a cycle in that the compiler_cli depends on Angular -# but we need it to compile Angular. -# The solution right now is to do 2 compilation runs. -# Fix this by separating the metadata extraction into a separate binary that does -# not depend on Angular. -$(npm bin)/tsc -p ./modules/tsconfig.json -node dist/all/@angular/compiler_cli/src/main -p modules/tsconfig.json +node dist/tools/tsc-wrapped/src/main -p modules/tsconfig.json # Compile the compiler_cli integration tests node dist/all/@angular/compiler_cli/src/main -p modules/@angular/compiler_cli/integrationtest diff --git a/tools/broccoli/broccoli-typescript.ts b/tools/broccoli/broccoli-typescript.ts index 777b7384b3..3e8930eb88 100644 --- a/tools/broccoli/broccoli-typescript.ts +++ b/tools/broccoli/broccoli-typescript.ts @@ -3,7 +3,7 @@ import fse = require('fs-extra'); import path = require('path'); import * as ts from 'typescript'; import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin'; -import {MetadataCollector} from '../ts-metadata-collector'; +import {MetadataCollector} from '../tsc-wrapped'; type FileRegistry = ts.Map<{version: number}>; diff --git a/tools/build/linknodemodules.js b/tools/build/linknodemodules.js index 101234a562..9c4f9eaf94 100644 --- a/tools/build/linknodemodules.js +++ b/tools/build/linknodemodules.js @@ -31,7 +31,7 @@ module.exports = function(gulp, plugins, config) { 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')); + symlink('../../tools/metadata', path.join(nodeModulesDir, 'tsc-wrapped')); }; }; diff --git a/tools/ts-metadata-collector/README.md b/tools/ts-metadata-collector/README.md deleted file mode 100644 index 6447457320..0000000000 --- a/tools/ts-metadata-collector/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# TypeScript Decorator metadata collector - -The `.d.ts` format does not preserve information about the Decorators applied to symbols. -Some tools, such as Angular 2 template compiler, need access to statically analyzable -information about Decorators, so this library allows programs to produce a `foo.metadata.json` -to accompany a `foo.d.ts` file, and preserves the information that was lost in the declaration -emit. - -## Releasing -``` -$ gulp build.tools -$ cp tools/metadata/package.json dist/tools/metadata/ -$ npm login [angularcore] -$ npm publish dist/tools/metadata -``` \ No newline at end of file diff --git a/tools/ts-metadata-collector/index.ts b/tools/ts-metadata-collector/index.ts deleted file mode 100644 index 5b41d0dcd3..0000000000 --- a/tools/ts-metadata-collector/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './src/collector'; -export * from './src/schema'; diff --git a/tools/tsc-watch/index.ts b/tools/tsc-watch/index.ts index cc51b52e6a..e8b48fe718 100644 --- a/tools/tsc-watch/index.ts +++ b/tools/tsc-watch/index.ts @@ -93,8 +93,7 @@ if (platform == 'node') { error: 'error', complete: 'Compilation complete. Watching for file changes.', onChangeCmds: [ - ['node', 'dist/tools/cjs-jasmine/index-tools', '--', 'ts-metadata-collector/**/*{_,.}spec.js'], - ['node', 'dist/tools/cjs-jasmine/index-tools', '--', 'public_api_guard/**/*{_,.}spec.js'] + ['node', 'dist/tools/cjs-jasmine/index-tools', '--', '{public_api_guard,tsc-wrapped}/**/*{_,.}spec.js'] ] }); } diff --git a/tools/tsc-wrapped/README.md b/tools/tsc-wrapped/README.md new file mode 100644 index 0000000000..ab657e6749 --- /dev/null +++ b/tools/tsc-wrapped/README.md @@ -0,0 +1,30 @@ +# tsc-wrapped + +This package is an internal dependency used by @angular/compiler-cli. Please use that instead. + +This is a wrapper around TypeScript's `tsc` program that allows us to hook in extra extensions. +TypeScript will eventually have an extensibility model for arbitrary extensions. We don't want +to constrain their design with baggage from a legacy implementation, so this wrapper only +supports specific extensions developed by the Angular team: + +- tsickle down-levels Decorators into Annotations so they can be tree-shaken +- tsickle can also optionally produce Closure Compiler-friendly code +- ./collector.ts emits an extra `.metadata.json` file for every `.d.ts` file written, + which retains metadata about decorators that is lost in the TS emit +- @angular/compiler-cli extends this library to additionally generate template code + +## TypeScript Decorator metadata collector + +The `.d.ts` format does not preserve information about the Decorators applied to symbols. +Some tools, such as Angular 2 template compiler, need access to statically analyzable +information about Decorators, so this library allows programs to produce a `foo.metadata.json` +to accompany a `foo.d.ts` file, and preserves the information that was lost in the declaration +emit. + +## Releasing +``` +$ $(npm bin)/tsc -p tools +$ cp tools/tsc-wrapped/package.json dist/tools/tsc-wrapped/ +$ npm login [angularcore] +$ npm publish dist/tools/tsc-wrapped +``` diff --git a/tools/tsc-wrapped/index.ts b/tools/tsc-wrapped/index.ts new file mode 100644 index 0000000000..fee8cafcde --- /dev/null +++ b/tools/tsc-wrapped/index.ts @@ -0,0 +1,5 @@ +export {TsickleHost, MetadataWriterHost} from './src/compiler_host'; +export {main, CodegenExtension} from './src/main'; +export {default as AngularCompilerOptions} from './src/options'; +export * from './src/collector'; +export * from './src/schema'; diff --git a/tools/ts-metadata-collector/package.json b/tools/tsc-wrapped/package.json similarity index 55% rename from tools/ts-metadata-collector/package.json rename to tools/tsc-wrapped/package.json index 04ed22aae2..7f932cfedb 100644 --- a/tools/ts-metadata-collector/package.json +++ b/tools/tsc-wrapped/package.json @@ -1,16 +1,19 @@ { - "name": "ts-metadata-collector", - "version": "0.1.1", - "description": "Collects static Decorator metadata from TypeScript sources", - "homepage": "https://github.com/angular/angular/tree/master/tools/metadata", + "name": "@angular/tsc-wrapped", + "version": "0.1.0", + "description": "Wraps the tsc CLI, allowing extensions.", + "homepage": "https://github.com/angular/angular/tree/master/tools/tsc-wrapped", "bugs": "https://github.com/angular/angular/issues", "contributors": [ + "Alex Eagle ", "Chuck Jazdzewski " ], "license": "MIT", "repository": {"type":"git","url":"https://github.com/angular/angular.git"}, - "devDependencies": {}, + "dependencies": { + "tsickle": "^0.1.2" + }, "peerDependencies": { - "typescript": "^1.8.9 || ^1.9.0-dev" + "typescript": "^1.9.0-dev" } } diff --git a/tools/ts-metadata-collector/src/collector.ts b/tools/tsc-wrapped/src/collector.ts similarity index 100% rename from tools/ts-metadata-collector/src/collector.ts rename to tools/tsc-wrapped/src/collector.ts diff --git a/modules/@angular/compiler_cli/src/compiler_host.ts b/tools/tsc-wrapped/src/compiler_host.ts similarity index 78% rename from modules/@angular/compiler_cli/src/compiler_host.ts rename to tools/tsc-wrapped/src/compiler_host.ts index 7d8dfab9df..8fb99da435 100644 --- a/modules/@angular/compiler_cli/src/compiler_host.ts +++ b/tools/tsc-wrapped/src/compiler_host.ts @@ -1,8 +1,7 @@ import * as ts from 'typescript'; -import * as path from 'path'; +import {writeFileSync} from 'fs'; import {convertDecorators} from 'tsickle'; -import {NodeReflectorHost} from './reflector_host'; -import {AngularCompilerOptions} from './codegen'; +import {MetadataCollector} from './collector'; /** * Implementation of CompilerHost that forwards all methods to another instance. @@ -38,7 +37,7 @@ interface DecoratorInvocation { args?: any[]; } `; - constructor(delegate: ts.CompilerHost, private options: ts.CompilerOptions) { super(delegate); } + constructor(delegate: ts.CompilerHost) { super(delegate); } getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => { @@ -52,17 +51,29 @@ interface DecoratorInvocation { newContent = converted.output + this.TSICKLE_SUPPORT; } return ts.createSourceFile(fileName, newContent, languageVersion, true); - } + }; } const IGNORED_FILES = /\.ngfactory\.js$|\.css\.js$|\.css\.shim\.js$/; export class MetadataWriterHost extends DelegatingHost { - private reflectorHost: NodeReflectorHost; - constructor(delegate: ts.CompilerHost, program: ts.Program, options: ts.CompilerOptions, - ngOptions: AngularCompilerOptions) { + private metadataCollector = new MetadataCollector(); + constructor(delegate: ts.CompilerHost, private program: ts.Program) { super(delegate); - this.reflectorHost = new NodeReflectorHost(program, this, options, ngOptions); + } + + private writeMetadata(emitFilePath: string, sourceFile: ts.SourceFile) { + // TODO: replace with DTS filePath when https://github.com/Microsoft/TypeScript/pull/8412 is + // released + if (/*DTS*/ /\.js$/.test(emitFilePath)) { + const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.metadata.json'); + const metadata = + this.metadataCollector.getMetadata(sourceFile, this.program.getTypeChecker()); + if (metadata && metadata.metadata) { + const metadataText = JSON.stringify(metadata); + writeFileSync(path, metadataText, {encoding: 'utf-8'}); + } + } } writeFile: ts.WriteFileCallback = (fileName: string, data: string, writeByteOrderMark: boolean, @@ -88,6 +99,6 @@ export class MetadataWriterHost extends DelegatingHost { if (sourceFiles.length > 1) { throw new Error('Bundled emit with --out is not supported'); } - this.reflectorHost.writeMetadata(fileName, sourceFiles[0]); - } + this.writeMetadata(fileName, sourceFiles[0]); + }; } diff --git a/tools/ts-metadata-collector/src/evaluator.ts b/tools/tsc-wrapped/src/evaluator.ts similarity index 100% rename from tools/ts-metadata-collector/src/evaluator.ts rename to tools/tsc-wrapped/src/evaluator.ts diff --git a/tools/tsc-wrapped/src/main.ts b/tools/tsc-wrapped/src/main.ts new file mode 100644 index 0000000000..eb617b742e --- /dev/null +++ b/tools/tsc-wrapped/src/main.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import {tsc, check} from './tsc'; +import NgOptions from './options'; +import {MetadataWriterHost, TsickleHost} from './compiler_host'; + +export type CodegenExtension = (ngOptions: NgOptions, program: ts.Program, host: ts.CompilerHost) => Promise; + +export function main(project: string, basePath?: string, + codegen?: CodegenExtension): Promise { + try { + let projectDir = project; + if (fs.lstatSync(project).isFile()) { + projectDir = path.dirname(project); + } + // file names in tsconfig are resolved relative to this absolute path + basePath = path.join(process.cwd(), basePath || projectDir); + + // read the configuration options from wherever you store them + const {parsed, ngOptions} = tsc.readConfiguration(project, basePath); + ngOptions.basePath = basePath; + + const host = ts.createCompilerHost(parsed.options, true); + const program = ts.createProgram(parsed.fileNames, parsed.options, host); + const errors = program.getOptionsDiagnostics(); + check(errors); + + if (ngOptions.skipTemplateCodegen || !codegen) { + codegen = () => Promise.resolve(null); + } + return codegen(ngOptions, program, host).then(() => { + tsc.typeCheck(host, program); + + // Emit *.js with Decorators lowered to Annotations, and also *.js.map + const tsicklePreProcessor = new TsickleHost(host); + tsc.emit(tsicklePreProcessor, program); + + if (!ngOptions.skipMetadataEmit) { + // Emit *.metadata.json and *.d.ts + // Not in the same emit pass with above, because tsickle erases + // decorators which we want to read or document. + // Do this emit second since TypeScript will create missing directories for us + // in the standard emit. + const metadataWriter = new MetadataWriterHost(host, program); + tsc.emit(metadataWriter, program); + } + }); + } catch (e) { + return Promise.reject(e); + } +} + +// CLI entry point +if (require.main === module) { + const args = require('minimist')(process.argv.slice(2)); + main(args.p || args.project || '.', args.basePath) + .then(exitCode => process.exit(exitCode)) + .catch(e => { + console.error(e.stack); + console.error("Compilation failed"); + process.exit(1); + }); +} diff --git a/tools/tsc-wrapped/src/options.ts b/tools/tsc-wrapped/src/options.ts new file mode 100644 index 0000000000..2034f5f553 --- /dev/null +++ b/tools/tsc-wrapped/src/options.ts @@ -0,0 +1,20 @@ +import * as ts from 'typescript'; + +interface Options extends ts.CompilerOptions { + // Absolute path to a directory where generated file structure is written + genDir: string; + + // Path to the directory containing the tsconfig.json file. + basePath: string; + + // Don't produce .metadata.json files (they don't work for bundled emit with --out) + skipMetadataEmit: boolean; + + // Don't produce .ngfactory.ts or .css.shim.ts files + skipTemplateCodegen: boolean; + + // Print extra information while running the compiler + trace: boolean; +} + +export default Options; diff --git a/tools/ts-metadata-collector/src/schema.ts b/tools/tsc-wrapped/src/schema.ts similarity index 100% rename from tools/ts-metadata-collector/src/schema.ts rename to tools/tsc-wrapped/src/schema.ts diff --git a/tools/ts-metadata-collector/src/symbols.ts b/tools/tsc-wrapped/src/symbols.ts similarity index 100% rename from tools/ts-metadata-collector/src/symbols.ts rename to tools/tsc-wrapped/src/symbols.ts diff --git a/modules/@angular/compiler_cli/src/tsc.ts b/tools/tsc-wrapped/src/tsc.ts similarity index 85% rename from modules/@angular/compiler_cli/src/tsc.ts rename to tools/tsc-wrapped/src/tsc.ts index c762f9f58f..67a69b2208 100644 --- a/modules/@angular/compiler_cli/src/tsc.ts +++ b/tools/tsc-wrapped/src/tsc.ts @@ -1,8 +1,6 @@ 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 AngularCompilerOptions from './options'; import {TsickleHost} from './compiler_host'; /** @@ -19,7 +17,6 @@ export interface CompilerInterface { } const DEBUG = false; -const SOURCE_EXTENSION = /\.[jt]s$/; function debug(msg: string, ...o: any[]) { if (DEBUG) console.log(msg, ...o); @@ -50,19 +47,24 @@ export class Tsc implements CompilerInterface { public parsed: ts.ParsedCommandLine; private basePath: string; + constructor(private readFile = ts.sys.readFile, private readDirectory = ts.sys.readDirectory) {} + readConfiguration(project: string, basePath: string) { this.basePath = basePath; // Allow a directory containing tsconfig.json as the project value - if (lstatSync(project).isDirectory()) { + try { + this.readDirectory(project); project = path.join(project, "tsconfig.json"); + } catch (e) { + // Was not a directory, continue on assuming it's a file } - const {config, error} = ts.readConfigFile(project, ts.sys.readFile); + const {config, error} = ts.readConfigFile(project, this.readFile); check([error]); this.parsed = - ts.parseJsonConfigFileContent(config, {readDirectory: ts.sys.readDirectory}, basePath); + ts.parseJsonConfigFileContent(config, {readDirectory: this.readDirectory}, basePath); check(this.parsed.errors); @@ -70,6 +72,9 @@ export class Tsc implements CompilerInterface { // Parsed options are already converted to absolute paths this.ngOptions = config.angularCompilerOptions || {}; this.ngOptions.genDir = path.join(basePath, this.ngOptions.genDir || '.'); + for (const key of Object.keys(this.parsed.options)) { + this.ngOptions[key] = this.parsed.options[key]; + } return {parsed: this.parsed, ngOptions: this.ngOptions}; } diff --git a/tools/ts-metadata-collector/test/collector.spec.ts b/tools/tsc-wrapped/test/collector.spec.ts similarity index 100% rename from tools/ts-metadata-collector/test/collector.spec.ts rename to tools/tsc-wrapped/test/collector.spec.ts diff --git a/tools/ts-metadata-collector/test/evaluator.spec.ts b/tools/tsc-wrapped/test/evaluator.spec.ts similarity index 100% rename from tools/ts-metadata-collector/test/evaluator.spec.ts rename to tools/tsc-wrapped/test/evaluator.spec.ts diff --git a/tools/ts-metadata-collector/test/symbols.spec.ts b/tools/tsc-wrapped/test/symbols.spec.ts similarity index 100% rename from tools/ts-metadata-collector/test/symbols.spec.ts rename to tools/tsc-wrapped/test/symbols.spec.ts diff --git a/tools/tsc-wrapped/test/tsc.spec.ts b/tools/tsc-wrapped/test/tsc.spec.ts new file mode 100644 index 0000000000..d81e427de5 --- /dev/null +++ b/tools/tsc-wrapped/test/tsc.spec.ts @@ -0,0 +1,28 @@ +import * as ts from 'typescript'; +import {Tsc} from '../src/tsc'; + +describe('options parsing', () => { + + const tsc = new Tsc(() => ` +{ + "angularCompilerOptions": { + "googleClosureOutput": true + }, + "compilerOptions": { + "module": "commonjs", + "outDir": "built" + } +}`, () => ['tsconfig.json']); + + it('should combine all options into ngOptions', () => { + const {parsed, ngOptions} = tsc.readConfiguration('projectDir', 'basePath'); + + expect(ngOptions).toEqual({ + genDir:'basePath', + googleClosureOutput: true, + module: ts.ModuleKind.CommonJS, + outDir: 'basePath/built', + configFilePath: undefined + }); + }); +}); diff --git a/tools/ts-metadata-collector/test/typescript.mocks.ts b/tools/tsc-wrapped/test/typescript.mocks.ts similarity index 100% rename from tools/ts-metadata-collector/test/typescript.mocks.ts rename to tools/tsc-wrapped/test/typescript.mocks.ts