diff --git a/package.json b/package.json index eeb8a187b145..a214163a9cb3 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@angular/platform-browser-dynamic": "^2.3.0", "@angular/platform-server": "^2.3.0", "@angular/router": "^3.3.0", + "@types/chalk": "^0.4.31", "@types/fs-extra": "0.0.37", "@types/glob": "^5.0.30", "@types/gulp": "^3.8.32", @@ -54,6 +55,7 @@ "autoprefixer": "^6.7.6", "axe-core": "^2.1.7", "axe-webdriverjs": "^0.5.0", + "chalk": "^1.1.3", "conventional-changelog": "^1.1.0", "dgeni": "^0.4.7", "dgeni-packages": "^0.16.5", diff --git a/src/lib/tsconfig-srcs.json b/src/lib/tsconfig-srcs.json deleted file mode 100644 index f90842dfd36f..000000000000 --- a/src/lib/tsconfig-srcs.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "lib": ["es6", "es2015", "dom"], - "module": "es2015", - "moduleResolution": "node", - "noEmitOnError": true, - "noImplicitAny": true, - "outDir": "../../dist/@angular/material", - "rootDir": ".", - "sourceMap": true, - "target": "es5", - "inlineSources": true, - "stripInternal": false, - "typeRoots": [ - "../../node_modules/@types/!(node)" - ] - }, - "exclude": [ - /* Exclude testing utilities in releases. */ - "core/testing/", - - "**/*.spec.*", - "system-config-spec.ts" - ], - "angularCompilerOptions": { - "genDir": "../../dist/@angular/material", - "skipTemplateCodegen": true, - "debug": true - } -} diff --git a/src/lib/tsconfig.json b/src/lib/tsconfig.json index 5940ef3eeb24..e5dc4507798a 100644 --- a/src/lib/tsconfig.json +++ b/src/lib/tsconfig.json @@ -4,19 +4,17 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "lib": ["es6", "es2015", "dom"], - "module": "commonjs", + "module": "es2015", "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, "outDir": "../../dist/@angular/material", "rootDir": ".", "sourceMap": true, - "target": "es5", + "target": "es6", "inlineSources": true, "stripInternal": false, "baseUrl": "", - "paths": { - }, "typeRoots": [ "../../node_modules/@types/!(node)" ], diff --git a/tools/gulp/tasks/aot.ts b/tools/gulp/tasks/aot.ts index 5b1b1796d97f..93baf4d38bb9 100644 --- a/tools/gulp/tasks/aot.ts +++ b/tools/gulp/tasks/aot.ts @@ -12,7 +12,7 @@ task('aot:copy', [':build:devapp:scss', ':build:devapp:assets']); */ task('aot:prepare', sequenceTask( 'clean', - ['aot:copy', 'build:components:release', ':build:components:ngc']) + ['aot:copy', 'build:components', ':build:components:ngc']) ); /** Builds the demo-app with the Angular compiler to verify that all components work. */ diff --git a/tools/gulp/tasks/components.ts b/tools/gulp/tasks/components.ts index 282b83f2aada..3d2468a2ea52 100644 --- a/tools/gulp/tasks/components.ts +++ b/tools/gulp/tasks/components.ts @@ -1,139 +1,150 @@ import {task, watch, src, dest} from 'gulp'; +import {ScriptTarget, ModuleKind} from 'typescript'; import * as path from 'path'; import { DIST_COMPONENTS_ROOT, PROJECT_ROOT, COMPONENTS_DIR, HTML_MINIFIER_OPTIONS, LICENSE_BANNER } from '../constants'; import { - sassBuildTask, tsBuildTask, execNodeTask, copyTask, sequenceTask, + sassBuildTask, tsBuildTask, execNodeTask, sequenceTask, triggerLivereload } from '../util/task_helpers'; -// No typings for these. +// There are no type definitions available for these imports. const inlineResources = require('../../../scripts/release/inline-resources'); const gulpRollup = require('gulp-better-rollup'); -const gulpMinifyCss = require('gulp-clean-css'); const gulpMinifyHtml = require('gulp-htmlmin'); const gulpIf = require('gulp-if'); +/** Path to tsconfig file for the components. */ +const tsconfigPath = path.join(COMPONENTS_DIR, 'tsconfig.json'); -// NOTE: there are two build "modes" in this file, based on which tsconfig is used. -// When `tsconfig.json` is used, we are outputting ES6 modules and a UMD bundle. This is used -// for serving and for release. -// -// When `tsconfig-spec.json` is used, we are outputting CommonJS modules. This is used -// for unit tests (karma). - -/** Path to the tsconfig used for ESM output. */ -const tsconfigPath = path.relative(PROJECT_ROOT, path.join(COMPONENTS_DIR, 'tsconfig-srcs.json')); +/** Asset files to be added to the components output. */ +const assetFiles = [ + path.join(COMPONENTS_DIR, '**/*.html'), + path.join(COMPONENTS_DIR, 'package.json'), + path.join(PROJECT_ROOT, 'README.md'), + path.join(PROJECT_ROOT, 'LICENSE'), +]; +/** Builds components to UMD bundle. */ +task('build:components', [':build:components:bundle:umd']); -/** [Watch task] Rebuilds (ESM output) whenever ts, scss, or html sources change. */ -task(':watch:components', () => { - watch(path.join(COMPONENTS_DIR, '**/*.ts'), ['build:components', triggerLivereload]); - watch(path.join(COMPONENTS_DIR, '**/*.scss'), ['build:components', triggerLivereload]); - watch(path.join(COMPONENTS_DIR, '**/*.html'), ['build:components', triggerLivereload]); -}); - +/** Builds components for Angular Material releases */ +task(':build:components:release', sequenceTask( + ':build:components:bundle:umd', + ':build:components:bundle:esm', + ':build:components:ngc' +)); -/** Builds component typescript only (ESM output). */ -task(':build:components:ts', tsBuildTask(path.join(COMPONENTS_DIR, 'tsconfig-srcs.json'))); +/** Builds components typescript in ES5, ES6 target. For specs Karma needs CJS output. */ +task(':build:components:ts:es5', tsBuildTask(tsconfigPath, { target: ScriptTarget.ES5 })); +task(':build:components:ts:es6', tsBuildTask(tsconfigPath, { target: ScriptTarget.ES6 })); +task(':build:components:ts:spec', tsBuildTask(tsconfigPath, { + target: ScriptTarget.ES5, module: ModuleKind.CommonJS +})); -/** Builds components typescript for tests (CJS output). */ -task(':build:components:spec', tsBuildTask(COMPONENTS_DIR)); +/** Tasks to create a UMD or ES bundle */ +task(':build:components:bundle:umd', sequenceTask( + ':build:components:ts:es5', ':build:components:inline', ':build:components:rollup:umd' +)); -/** Copies assets (html, markdown) to build output. */ -task(':build:components:assets', copyTask([ - path.join(COMPONENTS_DIR, '**/*.!(ts|spec.ts)'), - path.join(PROJECT_ROOT, 'README.md'), - path.join(PROJECT_ROOT, 'LICENSE'), -], DIST_COMPONENTS_ROOT)); +task(':build:components:bundle:esm', sequenceTask( + ':build:components:ts:es6', ':build:components:inline', ':build:components:rollup:esm' +)); -/** Minifies the HTML and CSS assets in the distribution folder. */ -task(':build:components:assets:minify', () => { - return src('**/*.+(html|css)', { cwd: DIST_COMPONENTS_ROOT}) - .pipe(gulpIf(/.css$/, gulpMinifyCss(), gulpMinifyHtml(HTML_MINIFIER_OPTIONS))) +/** Copies all component assets to the build output. */ +task(':build:components:assets', () => { + return src(assetFiles) + .pipe(gulpIf(/.html$/, gulpMinifyHtml(HTML_MINIFIER_OPTIONS))) .pipe(dest(DIST_COMPONENTS_ROOT)); }); -/** Builds scss into css. */ -task(':build:components:scss', sassBuildTask(DIST_COMPONENTS_ROOT, COMPONENTS_DIR)); - -/** Builds the UMD bundle for all of Angular Material. */ -task(':build:components:rollup', () => { - const globals: {[name: string]: string} = { - // Angular dependencies - '@angular/core': 'ng.core', - '@angular/common': 'ng.common', - '@angular/forms': 'ng.forms', - '@angular/http': 'ng.http', - '@angular/platform-browser': 'ng.platformBrowser', - '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', - - // Rxjs dependencies - 'rxjs/Subject': 'Rx', - 'rxjs/add/observable/fromEvent': 'Rx.Observable', - 'rxjs/add/observable/forkJoin': 'Rx.Observable', - 'rxjs/add/observable/of': 'Rx.Observable', - 'rxjs/add/observable/merge': 'Rx.Observable', - 'rxjs/add/observable/throw': 'Rx.Observable', - 'rxjs/add/operator/auditTime': 'Rx.Observable.prototype', - 'rxjs/add/operator/toPromise': 'Rx.Observable.prototype', - 'rxjs/add/operator/map': 'Rx.Observable.prototype', - 'rxjs/add/operator/filter': 'Rx.Observable.prototype', - 'rxjs/add/operator/do': 'Rx.Observable.prototype', - 'rxjs/add/operator/share': 'Rx.Observable.prototype', - 'rxjs/add/operator/finally': 'Rx.Observable.prototype', - 'rxjs/add/operator/catch': 'Rx.Observable.prototype', - 'rxjs/add/operator/first': 'Rx.Observable.prototype', - 'rxjs/add/operator/startWith': 'Rx.Observable.prototype', - 'rxjs/add/operator/switchMap': 'Rx.Observable.prototype', - 'rxjs/Observable': 'Rx' - }; - - const rollupOptions = { - context: 'this', - external: Object.keys(globals) - }; - - const rollupGenerateOptions = { - // Keep the moduleId empty because we don't want to force developers to a specific moduleId. - moduleId: '', - moduleName: 'ng.material', - format: 'umd', - globals, - banner: LICENSE_BANNER, - dest: 'material.umd.js' - }; +/** Compiles the components SCSS into minified CSS. */ +task(':build:components:scss', sassBuildTask(DIST_COMPONENTS_ROOT, COMPONENTS_DIR, true)); +/** Builds a ES6 bundle for all components. */ +task(':build:components:rollup:esm', () => { return src(path.join(DIST_COMPONENTS_ROOT, 'index.js')) - .pipe(gulpRollup(rollupOptions, rollupGenerateOptions)) + .pipe(createRollupBundle('es', 'material.js')) .pipe(dest(path.join(DIST_COMPONENTS_ROOT, 'bundles'))); }); -/** Builds components with resources (html, css) inlined into the built JS (ESM output). */ +/** Builds a UMD bundle (ES5) for all components. */ +task(':build:components:rollup:umd', () => { + return src(path.join(DIST_COMPONENTS_ROOT, 'index.js')) + .pipe(createRollupBundle('umd', 'material.umd.js')) + .pipe(dest(path.join(DIST_COMPONENTS_ROOT, 'bundles'))); +}); + + +/** Builds components with resources (html, css) inlined into the built JS. */ task(':build:components:inline', sequenceTask( - [':build:components:ts', ':build:components:scss', ':build:components:assets'], + [':build:components:scss', ':build:components:assets'], ':inline-resources', )); -/** Builds components with minified HTML and CSS inlined into the built JS. */ -task(':build:components:inline:release', sequenceTask( - [':build:components:ts', ':build:components:scss', ':build:components:assets'], - ':build:components:assets:minify', - ':inline-resources' -)); - -/** Inlines resources (html, css) into the JS output (for either ESM or CJS output). */ +/** Inlines resources (html, css) into the JS output. */ task(':inline-resources', () => inlineResources(DIST_COMPONENTS_ROOT)); -/** Builds components to ESM output and UMD bundle. */ -task('build:components', sequenceTask(':build:components:inline', ':build:components:rollup')); -task('build:components:release', sequenceTask( - ':build:components:inline:release', ':build:components:rollup' -)); - /** Generates metadata.json files for all of the components. */ -task(':build:components:ngc', ['build:components:release'], execNodeTask( +task(':build:components:ngc', ['build:components'], execNodeTask( '@angular/compiler-cli', 'ngc', ['-p', tsconfigPath] )); + +/** [Watch task] Rebuilds (ESM output) whenever ts, scss, or html sources change. */ +task(':watch:components', () => { + watch(path.join(COMPONENTS_DIR, '**/*.ts'), ['build:components', triggerLivereload]); + watch(path.join(COMPONENTS_DIR, '**/*.scss'), ['build:components', triggerLivereload]); + watch(path.join(COMPONENTS_DIR, '**/*.html'), ['build:components', triggerLivereload]); +}); + +const ROLLUP_GLOBALS = { + // Angular dependencies + '@angular/core': 'ng.core', + '@angular/common': 'ng.common', + '@angular/forms': 'ng.forms', + '@angular/http': 'ng.http', + '@angular/platform-browser': 'ng.platformBrowser', + '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', + + // Rxjs dependencies + 'rxjs/Subject': 'Rx', + 'rxjs/add/observable/fromEvent': 'Rx.Observable', + 'rxjs/add/observable/forkJoin': 'Rx.Observable', + 'rxjs/add/observable/of': 'Rx.Observable', + 'rxjs/add/observable/merge': 'Rx.Observable', + 'rxjs/add/observable/throw': 'Rx.Observable', + 'rxjs/add/operator/auditTime': 'Rx.Observable.prototype', + 'rxjs/add/operator/toPromise': 'Rx.Observable.prototype', + 'rxjs/add/operator/map': 'Rx.Observable.prototype', + 'rxjs/add/operator/filter': 'Rx.Observable.prototype', + 'rxjs/add/operator/do': 'Rx.Observable.prototype', + 'rxjs/add/operator/share': 'Rx.Observable.prototype', + 'rxjs/add/operator/finally': 'Rx.Observable.prototype', + 'rxjs/add/operator/catch': 'Rx.Observable.prototype', + 'rxjs/add/operator/first': 'Rx.Observable.prototype', + 'rxjs/add/operator/startWith': 'Rx.Observable.prototype', + 'rxjs/add/operator/switchMap': 'Rx.Observable.prototype', + 'rxjs/Observable': 'Rx' +}; + +/** Creates a rollup bundles of the Material components.*/ +function createRollupBundle(format: string, outFile: string) { + let rollupOptions = { + context: 'this', + external: Object.keys(ROLLUP_GLOBALS) + }; + + let rollupGenerateOptions = { + // Keep the moduleId empty because we don't want to force developers to a specific moduleId. + moduleId: '', + moduleName: 'ng.material', + banner: LICENSE_BANNER, + format: format, + dest: outFile, + globals: ROLLUP_GLOBALS, + }; + + return gulpRollup(rollupOptions, rollupGenerateOptions); +} diff --git a/tools/gulp/tasks/development.ts b/tools/gulp/tasks/development.ts index c8d318899b0d..bb3265236f1b 100644 --- a/tools/gulp/tasks/development.ts +++ b/tools/gulp/tasks/development.ts @@ -17,9 +17,11 @@ task(':watch:devapp', () => { watch(path.join(appDir, '**/*.html'), [':build:devapp:assets', triggerLivereload]); }); +/** Path to the demo-app tsconfig file. */ +const tsconfigPath = path.join(appDir, 'tsconfig.json'); task(':build:devapp:vendor', vendorTask()); -task(':build:devapp:ts', tsBuildTask(appDir)); +task(':build:devapp:ts', tsBuildTask(tsconfigPath)); task(':build:devapp:scss', sassBuildTask(outDir, appDir)); task(':build:devapp:assets', copyTask(appDir, outDir)); task('build:devapp', buildAppTask('devapp')); diff --git a/tools/gulp/tasks/docs.ts b/tools/gulp/tasks/docs.ts index aaee62a585b0..00b0a40bf274 100644 --- a/tools/gulp/tasks/docs.ts +++ b/tools/gulp/tasks/docs.ts @@ -3,7 +3,7 @@ import {Dgeni} from 'dgeni'; import * as path from 'path'; import {HTML_MINIFIER_OPTIONS} from '../constants'; -// Node packages that lack of types. +// There are no type definitions available for these imports. const markdown = require('gulp-markdown'); const transform = require('gulp-transform'); const highlight = require('gulp-highlight-files'); diff --git a/tools/gulp/tasks/e2e.ts b/tools/gulp/tasks/e2e.ts index 009dc1bbe3a0..2cd78f2c0f34 100644 --- a/tools/gulp/tasks/e2e.ts +++ b/tools/gulp/tasks/e2e.ts @@ -14,6 +14,8 @@ const appDir = path.join(SOURCE_ROOT, 'e2e-app'); const outDir = DIST_ROOT; const PROTRACTOR_CONFIG_PATH = path.join(PROJECT_ROOT, 'test/protractor.conf.js'); +const tsconfigPath = path.join(appDir, 'tsconfig.json'); + task(':watch:e2eapp', () => { watch(path.join(appDir, '**/*.ts'), [':build:e2eapp:ts']); watch(path.join(appDir, '**/*.html'), [':build:e2eapp:assets']); @@ -23,7 +25,7 @@ task(':watch:e2eapp', () => { task(':build:e2eapp:vendor', vendorTask()); /** Builds e2e app ts to js. */ -task(':build:e2eapp:ts', tsBuildTask(appDir)); +task(':build:e2eapp:ts', tsBuildTask(tsconfigPath)); /** Copies e2e app assets (html, css) to build output. */ task(':build:e2eapp:assets', copyTask(appDir, outDir)); diff --git a/tools/gulp/tasks/payload.ts b/tools/gulp/tasks/payload.ts index d7054da3621a..fcb2e3f245c9 100644 --- a/tools/gulp/tasks/payload.ts +++ b/tools/gulp/tasks/payload.ts @@ -6,7 +6,7 @@ import {spawnSync} from 'child_process'; import {isTravisPushBuild} from '../util/travis-ci'; import {openFirebaseDashboardDatabase} from '../util/firebase'; -// Those imports lack types. +// There are no type definitions available for these imports. const uglifyJs = require('uglify-js'); const BUNDLE_PATH = join(DIST_COMPONENTS_ROOT, 'bundles', 'material.umd.js'); diff --git a/tools/gulp/tasks/release.ts b/tools/gulp/tasks/release.ts index e6bcce200de4..a02c62cfc580 100644 --- a/tools/gulp/tasks/release.ts +++ b/tools/gulp/tasks/release.ts @@ -19,7 +19,7 @@ task('build:release', function(done: () => void) { // Synchronously run those tasks. gulpRunSequence( 'clean', - ':build:components:ngc', + ':build:components:release', ':build:release:clean-spec', done ); diff --git a/tools/gulp/tasks/unit-test.ts b/tools/gulp/tasks/unit-test.ts index 40908baf9301..dda92efb825c 100644 --- a/tools/gulp/tasks/unit-test.ts +++ b/tools/gulp/tasks/unit-test.ts @@ -29,7 +29,7 @@ gulp.task(':test:deps', sequenceTask( ':build:test:vendor', ':build:components:assets', ':build:components:scss', - ':build:components:spec', + ':build:components:ts:spec', ] )); @@ -84,7 +84,7 @@ gulp.task('test', [':test:deps'], () => { server.on('browser_register', runTests); // Watch for file changes, rebuild and run the tests. - gulp.watch(patternRoot + '.ts', () => runSequence(':build:components:spec', runTests)); + gulp.watch(patternRoot + '.ts', () => runSequence(':build:components:ts:spec', runTests)); gulp.watch(patternRoot + '.scss', () => runSequence(':build:components:scss', runTests)); gulp.watch(patternRoot + '.html', () => runSequence(':build:components:assets', runTests)); }); diff --git a/tools/gulp/util/task_helpers.ts b/tools/gulp/util/task_helpers.ts index 8558d3072703..8913ef2a44be 100644 --- a/tools/gulp/util/task_helpers.ts +++ b/tools/gulp/util/task_helpers.ts @@ -3,18 +3,20 @@ import * as fs from 'fs'; import * as gulp from 'gulp'; import * as path from 'path'; import {NPM_VENDOR_FILES, PROJECT_ROOT, DIST_ROOT} from '../constants'; +import {CompilerOptions} from 'typescript'; +import {compileProject} from './ts-compiler'; - -/** Those imports lack typings. */ +/* Those imports lack typings. */ const gulpClean = require('gulp-clean'); const gulpMerge = require('merge2'); const gulpRunSequence = require('run-sequence'); const gulpSass = require('gulp-sass'); const gulpSourcemaps = require('gulp-sourcemaps'); const gulpConnect = require('gulp-connect'); -const resolveBin = require('resolve-bin'); - +const gulpIf = require('gulp-if'); +const gulpCleanCss = require('gulp-clean-css'); +const resolveBin = require('resolve-bin'); /** If the string passed in is a glob, returns it, otherwise append '**\/*' to it. */ function _globify(maybeGlob: string, suffix = '**/*') { @@ -32,17 +34,20 @@ function _globify(maybeGlob: string, suffix = '**/*') { /** Creates a task that runs the TypeScript compiler */ -export function tsBuildTask(tsConfigPath: string) { - return execNodeTask('typescript', 'tsc', ['-p', tsConfigPath]); +export function tsBuildTask(tsConfigPath: string, extraOptions?: CompilerOptions) { + return () => { + compileProject(tsConfigPath, extraOptions); + }; } /** Create a SASS Build Task. */ -export function sassBuildTask(dest: string, root: string) { +export function sassBuildTask(dest: string, root: string, minify = false) { return () => { return gulp.src(_globify(root, '**/*.scss')) - .pipe(gulpSourcemaps.init()) + .pipe(gulpSourcemaps.init({ loadMaps: true })) .pipe(gulpSass().on('error', gulpSass.logError)) + .pipe(gulpIf(minify, gulpCleanCss())) .pipe(gulpSourcemaps.write('.')) .pipe(gulp.dest(dest)); }; diff --git a/tools/gulp/util/ts-compiler.ts b/tools/gulp/util/ts-compiler.ts new file mode 100644 index 000000000000..6045b7aee64b --- /dev/null +++ b/tools/gulp/util/ts-compiler.ts @@ -0,0 +1,56 @@ +import * as ts from 'typescript'; +import * as path from 'path'; +import * as chalk from 'chalk'; + +/** Compiles a TypeScript project with possible extra options. */ +export function compileProject(project: string, options: ts.CompilerOptions) { + let parsed = parseProjectConfig(project, options); + let program = ts.createProgram(parsed.fileNames, parsed.options); + + // Report any invalid TypeScript options for the project. + checkDiagnostics(program.getOptionsDiagnostics()); + + let emitResult = program.emit(); + + checkDiagnostics(emitResult.diagnostics); +} + +/** Parses a TypeScript project configuration. */ +function parseProjectConfig(project: string, options: ts.CompilerOptions) { + let config = ts.readConfigFile(project, ts.sys.readFile).config; + let basePath = path.dirname(project); + + let host = { + useCaseSensitiveFileNames: true, + fileExists: ts.sys.fileExists, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile + }; + + return ts.parseJsonConfigFileContent(config, host, basePath, options); +} + +/** Formats the TypeScript diagnostics into a error string. */ +export function formatDiagnostics(diagnostics: ts.Diagnostic[]): string { + return diagnostics.map(diagnostic => { + let res = ts.DiagnosticCategory[diagnostic.category]; + + if (diagnostic.file) { + let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + + res += ` at ${diagnostic.file.fileName}(${line + 1},${character + 1}):`; + } + res += ` ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`; + + return res; + }).join('\n'); +} + +/** Checks diagnostics and throws errors if present. */ +export function checkDiagnostics(diagnostics: ts.Diagnostic[]) { + if (diagnostics && diagnostics.length && diagnostics[0]) { + console.error(formatDiagnostics(diagnostics)); + console.error(chalk.red('TypeScript compilation failed. Exiting process.')); + process.exit(1); + } +}