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..b1d36f709358 --- /dev/null +++ b/tools/gulp/util/sub-packages.ts @@ -0,0 +1,109 @@ +import {join, basename, normalize, isAbsolute, dirname, relative} from 'path'; +import {sync as glob} from 'glob'; +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'; +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 a 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/${rootPackage}`, + // 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 the 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 fesm2015File = join(fesmOutputDir, `${packageName}.js`); + const fesm2014File = join(fesmOutputDir, `${packageName}.es5.js`); + + const moduleIndexPath = join(fesmOutputDir, `module-index.js`); + + let indexContent = `export * from '../../../packages/${packageName}/module';\n`; + + indexContent += bundlePaths.map(bundle => `export * from './${basename(bundle)}';`).join('\n'); + + // 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: fesm2015File, + format: 'es', + external: importPath => importPath !== moduleIndexPath && !importPath.includes('module'), + paths: importPath => { + if (importPath.includes(packagePath)) { return `@angular/${packageName}`; } + if (dirname(importPath) === fesmOutputDir) { return 'TEST (intentionally committed!)'; } + } + }); + + + // Downlevel the FESM-2015 file to ES5. + transpileFile(fesm2015File, fesm2014File, { + target: ScriptTarget.ES5, + module: ModuleKind.ES2015, + allowJs: true + }); +}