From 5de54ca48fdb4f8c8f47cc057cf5884586c9d5bf Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 28 Apr 2017 19:35:09 +0200 Subject: [PATCH 1/4] build: ship individual component bundles * Splits the FESM bundles into separate bundles for each component. This should make tree-shaking available for Material. --- tools/gulp/tasks/material-release.ts | 13 +++- tools/gulp/util/package-build.ts | 4 +- tools/gulp/util/rollup-helper.ts | 9 ++- tools/gulp/util/sub-packages.ts | 97 ++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 tools/gulp/util/sub-packages.ts diff --git a/tools/gulp/tasks/material-release.ts b/tools/gulp/tasks/material-release.ts index 693e2a9934fc..2a2e2e94e81e 100644 --- a/tools/gulp/tasks/material-release.ts +++ b/tools/gulp/tasks/material-release.ts @@ -2,9 +2,10 @@ import {task, src, dest} from 'gulp'; import {join} from 'path'; import {writeFileSync} from 'fs'; import {Bundler} from 'scss-bundle'; -import {execNodeTask, sequenceTask} from '../util/task_helpers'; +import {sequenceTask} from '../util/task_helpers'; import {composeRelease} from '../util/package-build'; import {COMPONENTS_DIR, DIST_MATERIAL, DIST_RELEASES} from '../constants'; +import {composeSubpackages, buildAllSubpackages} from '../util/sub-packages'; // There are no type definitions available for these imports. const gulpRename = require('gulp-rename'); @@ -24,7 +25,10 @@ const allScssGlob = join(COMPONENTS_DIR, '**/*.scss'); * Overwrite the release task for the material package. The material release will include special * files, like a bundled theming SCSS file or all prebuilt themes. */ -task('material:build-release', ['material:prepare-release'], () => composeRelease('material')); +task('material:build-release', ['material:prepare-release'], async () => { + await composeRelease('material'); + await composeSubpackages('material'); +}); /** * Task that will build the material package. It will also copy all prebuilt themes and build @@ -32,9 +36,12 @@ task('material:build-release', ['material:prepare-release'], () => composeReleas */ task('material:prepare-release', sequenceTask( 'material:build', - ['material:copy-prebuilt-themes', 'material:bundle-theming-scss'] + ['material:copy-prebuilt-themes', 'material:bundle-theming-scss', 'material:build-subpackages'] )); +/** Task that builds all subpackages of the Material package. Necessary for releases. */ +task('material:build-subpackages', () => buildAllSubpackages('material')); + /** Copies all prebuilt themes into the release package under `prebuilt-themes/` */ task('material:copy-prebuilt-themes', () => { src(prebuiltThemeGlob) diff --git a/tools/gulp/util/package-build.ts b/tools/gulp/util/package-build.ts index c4b0b97c1534..23a752ceac3c 100644 --- a/tools/gulp/util/package-build.ts +++ b/tools/gulp/util/package-build.ts @@ -90,7 +90,7 @@ export async function buildPackageBundles(entryFile: string, packageName: string * Finds the original sourcemap of the file and maps it to the current file. * This is useful when multiple transformation happen (e.g TSC -> Rollup -> Uglify) **/ -async function remapSourcemap(sourceFile: string) { +export async function remapSourcemap(sourceFile: string) { // Once sorcery loaded the chain of sourcemaps, the new sourcemap will be written asynchronously. return (await sorcery.load(sourceFile)).write(); } @@ -107,7 +107,7 @@ function uglifyFile(inputPath: string, outputPath: string) { writeFileSync(sourcemapOut, result.map); } -function copyFiles(fromPath: string, fileGlob: string, outDir: string) { +export function copyFiles(fromPath: string, fileGlob: string, outDir: string) { glob(fileGlob, {cwd: fromPath}).forEach(filePath => { let fileDestPath = join(outDir, filePath); mkdirpSync(dirname(fileDestPath)); diff --git a/tools/gulp/util/rollup-helper.ts b/tools/gulp/util/rollup-helper.ts index 32f85561bb43..425943f5df92 100644 --- a/tools/gulp/util/rollup-helper.ts +++ b/tools/gulp/util/rollup-helper.ts @@ -3,7 +3,7 @@ import {LICENSE_BANNER} from '../constants'; // There are no type definitions available for these imports. const rollup = require('rollup'); -const ROLLUP_GLOBALS = { +export const ROLLUP_GLOBALS = { // Angular dependencies '@angular/animations': 'ng.animations', '@angular/core': 'ng.core', @@ -44,14 +44,17 @@ export type BundleConfig = { dest: string; format: string; moduleName: string; + external?: (moduleId: string) => boolean; + paths?: (moduleId: string) => string; }; /** Creates a rollup bundles of the Material components.*/ export function createRollupBundle(config: BundleConfig): Promise { let bundleOptions = { context: 'this', - external: Object.keys(ROLLUP_GLOBALS), - entry: config.entry + external: config.external || Object.keys(ROLLUP_GLOBALS), + entry: config.entry, + paths: config.paths }; let writeOptions = { diff --git a/tools/gulp/util/sub-packages.ts b/tools/gulp/util/sub-packages.ts new file mode 100644 index 000000000000..acb5b420839a --- /dev/null +++ b/tools/gulp/util/sub-packages.ts @@ -0,0 +1,97 @@ +import {join, basename, normalize, isAbsolute,} from 'path'; +import {sync as glob} from 'glob'; +import {DIST_ROOT, DIST_BUNDLES, DIST_RELEASES, LICENSE_BANNER} from '../constants'; +import {remapSourcemap, copyFiles} from './package-build'; +import {createRollupBundle, ROLLUP_GLOBALS} from './rollup-helper'; +import {transpileFile} from './ts-compiler'; +import {ScriptTarget, ModuleKind} from 'typescript'; +import {writeFileSync} from 'fs'; + +/** Modules that will be always treated as external */ +const ROLLUP_EXTERNALS = Object.keys(ROLLUP_GLOBALS); + +/** + * Builds FESM bundles for all available sub-packages of the package. A subpackage is every + * directory inside of the root package. + */ +export async function buildAllSubpackages(packageName: string) { + const packageOut = join(DIST_ROOT, 'packages', packageName); + const subPackages = glob(join(packageOut, '*/')); + + await Promise.all(subPackages.map(packagePath => { + return buildSubpackage(basename(packagePath), packagePath, packageName); + })); +} + +/** + * Builds FESM bundles for the specified sub-package of a package. Subpackage bundles are + * used to improve tree-shaking in bundlers like Webpack. + */ +async function buildSubpackage(packageName: string, packagePath: string, rootPackage: string) { + const entryFile = join(packagePath, 'index.js'); + const moduleName = `ng.${rootPackage}.${packageName}`; + + // List of paths to the subpackage bundles. + const fesm2015File = join(DIST_BUNDLES, rootPackage, `${packageName}.js`); + const fesm2014File = join(DIST_BUNDLES, rootPackage, `${packageName}.es5.js`); + + // Build FESM-2015 bundle for the subpackage folder. + await createRollupBundle({ + moduleName: moduleName, + entry: entryFile, + dest: fesm2015File, + format: 'es', + // Rewrite all internal paths to scoped package imports + paths: importPath => importPath.includes(DIST_ROOT) && `@angular/${packageName}`, + // Function to only bundle all files inside of the current subpackage. + external: importPath => { + return ROLLUP_EXTERNALS.indexOf(importPath) !== -1 || + isAbsolute(importPath) && !importPath.includes(packageName); + }, + }); + + await remapSourcemap(fesm2015File); + + // Downlevel FESM-2015 file to ES5. + transpileFile(fesm2015File, fesm2014File, { + target: ScriptTarget.ES5, + module: ModuleKind.ES2015, + allowJs: true + }); + + await remapSourcemap(fesm2014File); +} + +/** + * Function that composes a release with the subpackages included. Needs to run after the main + * compose release function. + */ +export async function composeSubpackages(packageName: string) { + const packagePath = join(DIST_ROOT, 'packages', packageName); + const bundlesDir = join(DIST_BUNDLES, packageName); + const bundlePaths = glob(join(bundlesDir, '!(*.es5).js')).map(normalize); + + const fesmOutputDir = join(DIST_RELEASES, packageName, '@angular'); + const fesmOutputPath = join(fesmOutputDir, `${packageName}.js`); + const moduleIndexPath = join(fesmOutputDir, `module-index.js`); + + let indexContent = bundlePaths.map(bundle => `export * from './${basename(bundle)}';`).join('\n'); + + // Manually add the `module.ts` file for now. This is not very dynamic but also a special case. + indexContent += `\nexport * from '../../../packages/${packageName}/module';`; + + // Write index file to disk. Afterwards run rollup to bundle. + writeFileSync(moduleIndexPath, indexContent); + + // Copy all bundles into the release directory. + copyFiles(bundlesDir, '*.js', fesmOutputDir); + + await createRollupBundle({ + moduleName: `ng.${packageName}`, + entry: moduleIndexPath, + dest: fesmOutputPath, + format: 'es', + // Function to only bundle all files inside of the root package directory. + external: importPath => importPath !== moduleIndexPath + }); +} From 049206983b4cf973e02c5f24264971bc1428aa62 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 28 Apr 2017 20:08:17 +0200 Subject: [PATCH 2/4] Include module file in bundle --- tools/gulp/util/sub-packages.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/gulp/util/sub-packages.ts b/tools/gulp/util/sub-packages.ts index acb5b420839a..5c8333e715c4 100644 --- a/tools/gulp/util/sub-packages.ts +++ b/tools/gulp/util/sub-packages.ts @@ -42,7 +42,7 @@ async function buildSubpackage(packageName: string, packagePath: string, rootPac dest: fesm2015File, format: 'es', // Rewrite all internal paths to scoped package imports - paths: importPath => importPath.includes(DIST_ROOT) && `@angular/${packageName}`, + paths: importPath => importPath.includes(DIST_ROOT) && `@angular/${rootPackage}`, // Function to only bundle all files inside of the current subpackage. external: importPath => { return ROLLUP_EXTERNALS.indexOf(importPath) !== -1 || @@ -91,7 +91,7 @@ export async function composeSubpackages(packageName: string) { entry: moduleIndexPath, dest: fesmOutputPath, format: 'es', - // Function to only bundle all files inside of the root package directory. - external: importPath => importPath !== moduleIndexPath + external: importPath => importPath !== moduleIndexPath && !importPath.includes('module'), + paths: importPath => importPath.includes(packagePath) && `@angular/${packageName}` }); } From 5da502ba316c2397bae8e9ee446c994405cba5dd Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 28 Apr 2017 20:13:22 +0200 Subject: [PATCH 3/4] Overwrite ES5 bundle with subpackages --- tools/gulp/util/sub-packages.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tools/gulp/util/sub-packages.ts b/tools/gulp/util/sub-packages.ts index 5c8333e715c4..65da98b580c1 100644 --- a/tools/gulp/util/sub-packages.ts +++ b/tools/gulp/util/sub-packages.ts @@ -35,7 +35,7 @@ async function buildSubpackage(packageName: string, packagePath: string, rootPac const fesm2015File = join(DIST_BUNDLES, rootPackage, `${packageName}.js`); const fesm2014File = join(DIST_BUNDLES, rootPackage, `${packageName}.es5.js`); - // Build FESM-2015 bundle for the subpackage folder. + // Build a FESM-2015 bundle for the subpackage folder. await createRollupBundle({ moduleName: moduleName, entry: entryFile, @@ -52,7 +52,7 @@ async function buildSubpackage(packageName: string, packagePath: string, rootPac await remapSourcemap(fesm2015File); - // Downlevel FESM-2015 file to ES5. + // Downlevel the FESM-2015 file to ES5. transpileFile(fesm2015File, fesm2014File, { target: ScriptTarget.ES5, module: ModuleKind.ES2015, @@ -72,7 +72,9 @@ export async function composeSubpackages(packageName: string) { const bundlePaths = glob(join(bundlesDir, '!(*.es5).js')).map(normalize); const fesmOutputDir = join(DIST_RELEASES, packageName, '@angular'); - const fesmOutputPath = join(fesmOutputDir, `${packageName}.js`); + const fesm2015File = join(fesmOutputDir, `${packageName}.js`); + const fesm2014File = join(fesmOutputDir, `${packageName}.es5.js`); + const moduleIndexPath = join(fesmOutputDir, `module-index.js`); let indexContent = bundlePaths.map(bundle => `export * from './${basename(bundle)}';`).join('\n'); @@ -89,9 +91,17 @@ export async function composeSubpackages(packageName: string) { await createRollupBundle({ moduleName: `ng.${packageName}`, entry: moduleIndexPath, - dest: fesmOutputPath, + dest: fesm2015File, format: 'es', external: importPath => importPath !== moduleIndexPath && !importPath.includes('module'), paths: importPath => importPath.includes(packagePath) && `@angular/${packageName}` }); + + + // Downlevel the FESM-2015 file to ES5. + transpileFile(fesm2015File, fesm2014File, { + target: ScriptTarget.ES5, + module: ModuleKind.ES2015, + allowJs: true + }); } From 7ae8d622a831683818a60de462fd88a408a4f40c Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 28 Apr 2017 20:35:42 +0200 Subject: [PATCH 4/4] More testing --- tools/gulp/util/sub-packages.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/gulp/util/sub-packages.ts b/tools/gulp/util/sub-packages.ts index 65da98b580c1..b1d36f709358 100644 --- a/tools/gulp/util/sub-packages.ts +++ b/tools/gulp/util/sub-packages.ts @@ -1,6 +1,6 @@ -import {join, basename, normalize, isAbsolute,} from 'path'; +import {join, basename, normalize, isAbsolute, dirname, relative} from 'path'; import {sync as glob} from 'glob'; -import {DIST_ROOT, DIST_BUNDLES, DIST_RELEASES, LICENSE_BANNER} from '../constants'; +import {DIST_ROOT, DIST_BUNDLES, DIST_RELEASES} from '../constants'; import {remapSourcemap, copyFiles} from './package-build'; import {createRollupBundle, ROLLUP_GLOBALS} from './rollup-helper'; import {transpileFile} from './ts-compiler'; @@ -77,10 +77,9 @@ export async function composeSubpackages(packageName: string) { const moduleIndexPath = join(fesmOutputDir, `module-index.js`); - let indexContent = bundlePaths.map(bundle => `export * from './${basename(bundle)}';`).join('\n'); + let indexContent = `export * from '../../../packages/${packageName}/module';\n`; - // Manually add the `module.ts` file for now. This is not very dynamic but also a special case. - indexContent += `\nexport * from '../../../packages/${packageName}/module';`; + indexContent += bundlePaths.map(bundle => `export * from './${basename(bundle)}';`).join('\n'); // Write index file to disk. Afterwards run rollup to bundle. writeFileSync(moduleIndexPath, indexContent); @@ -94,7 +93,10 @@ export async function composeSubpackages(packageName: string) { dest: fesm2015File, format: 'es', external: importPath => importPath !== moduleIndexPath && !importPath.includes('module'), - paths: importPath => importPath.includes(packagePath) && `@angular/${packageName}` + paths: importPath => { + if (importPath.includes(packagePath)) { return `@angular/${packageName}`; } + if (dirname(importPath) === fesmOutputDir) { return 'TEST (intentionally committed!)'; } + } });