build: ship esm bundle (#3443)

* build: ship esm bundle

* Now ships a Flat / ESM bundle (similar as for Angular 4+ releases)
* In future PRs we will remove the single ES-files and can just link to the ES bundle (as in angular/angular).

* Use template strings for diagnostic
This commit is contained in:
Paul Gschwendtner
2017-03-17 21:25:01 +01:00
committed by mmalerba
parent f526c9de87
commit b810e11fa4
13 changed files with 198 additions and 155 deletions
+2
View File
@@ -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",
-33
View File
@@ -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
}
}
+2 -4
View File
@@ -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)"
],
+1 -1
View File
@@ -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. */
+113 -102
View File
@@ -1,32 +1,96 @@
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).
/** 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'),
];
/** Path to the tsconfig used for ESM output. */
const tsconfigPath = path.relative(PROJECT_ROOT, path.join(COMPONENTS_DIR, 'tsconfig-srcs.json'));
/** Builds components to UMD bundle. */
task('build:components', [':build:components:bundle:umd']);
/** Builds components for Angular Material releases */
task(':build:components:release', sequenceTask(
':build:components:bundle:umd',
':build:components:bundle:esm',
':build:components:ngc'
));
/** 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
}));
/** 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'
));
task(':build:components:bundle:esm', sequenceTask(
':build:components:ts:es6', ':build:components:inline', ':build:components:rollup:esm'
));
/** 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));
});
/** 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(createRollupBundle('es', 'material.js'))
.pipe(dest(path.join(DIST_COMPONENTS_ROOT, 'bundles')));
});
/** 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:scss', ':build:components:assets'],
':inline-resources',
));
/** Inlines resources (html, css) into the JS output. */
task(':inline-resources', () => inlineResources(DIST_COMPONENTS_ROOT));
/** Generates metadata.json files for all of the components. */
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', () => {
@@ -35,105 +99,52 @@ task(':watch:components', () => {
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',
/** Builds component typescript only (ESM output). */
task(':build:components:ts', tsBuildTask(path.join(COMPONENTS_DIR, 'tsconfig-srcs.json')));
// 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'
};
/** Builds components typescript for tests (CJS output). */
task(':build:components:spec', tsBuildTask(COMPONENTS_DIR));
/** 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));
/** 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)))
.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 = {
/** Creates a rollup bundles of the Material components.*/
function createRollupBundle(format: string, outFile: string) {
let rollupOptions = {
context: 'this',
external: Object.keys(globals)
external: Object.keys(ROLLUP_GLOBALS)
};
const rollupGenerateOptions = {
let 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'
format: format,
dest: outFile,
globals: ROLLUP_GLOBALS,
};
return src(path.join(DIST_COMPONENTS_ROOT, 'index.js'))
.pipe(gulpRollup(rollupOptions, rollupGenerateOptions))
.pipe(dest(path.join(DIST_COMPONENTS_ROOT, 'bundles')));
});
/** Builds components with resources (html, css) inlined into the built JS (ESM output). */
task(':build:components:inline', sequenceTask(
[':build:components:ts', ':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). */
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(
'@angular/compiler-cli', 'ngc', ['-p', tsconfigPath]
));
return gulpRollup(rollupOptions, rollupGenerateOptions);
}
+3 -1
View File
@@ -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'));
+1 -1
View File
@@ -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');
+3 -1
View File
@@ -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));
+1 -1
View File
@@ -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');
+1 -1
View File
@@ -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
);
+2 -2
View File
@@ -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));
});
+13 -8
View File
@@ -3,19 +3,21 @@ 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 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 = '**/*') {
if (maybeGlob.indexOf('*') != -1) {
@@ -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));
};
+56
View File
@@ -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);
}
}