diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index 979c4115e..866752e34 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -1,7 +1,8 @@ import {createPackageBuildTasks} from 'lib-build-tools'; +import {flexLayoutPackage} from './packages'; /** Create gulp tasks to build the different packages in the project. */ -createPackageBuildTasks('flex-layout'); +createPackageBuildTasks(flexLayoutPackage); import './tasks/aot'; import './tasks/build-release'; // release build `github.com/angular/flex-layout-builds` diff --git a/tools/gulp/packages.ts b/tools/gulp/packages.ts new file mode 100644 index 000000000..68eabbc0c --- /dev/null +++ b/tools/gulp/packages.ts @@ -0,0 +1,7 @@ +import {BuildPackage, buildConfig} from 'lib-build-tools'; +import {join} from 'path'; + +export const flexLayoutPackage = new BuildPackage('flex-layout', []); + +// To avoid refactoring of the project the Flex-Layout package will map to the source path `lib/`. +flexLayoutPackage.packageRoot = join(buildConfig.packagesDir, 'lib'); diff --git a/tools/gulp/tasks/build-release.ts b/tools/gulp/tasks/build-release.ts index 53b6112c7..b9a5ea5f7 100644 --- a/tools/gulp/tasks/build-release.ts +++ b/tools/gulp/tasks/build-release.ts @@ -1,11 +1,11 @@ import {task} from 'gulp'; import {composeRelease, sequenceTask} from 'lib-build-tools'; - +import {flexLayoutPackage} from '../packages'; /** * Overwrite the release task for the Flex-Layout package. The Flex-Layout release will include special * files, like a bundled theming SCSS file or all prebuilt themes. */ -task('flex-layout:build-release', ['flex-layout:prepare-release'], () => composeRelease('flex-layout')); +task('flex-layout:build-release', ['flex-layout:prepare-release'], () => composeRelease(flexLayoutPackage)); /** * Task that will build the Flex-Layout package. It will also copy all prebuilt themes and build diff --git a/tools/gulp/tasks/development.ts b/tools/gulp/tasks/development.ts index a67128883..c472c5a62 100644 --- a/tools/gulp/tasks/development.ts +++ b/tools/gulp/tasks/development.ts @@ -1,8 +1,8 @@ -import {task, watch} from 'gulp'; +import {task} from 'gulp'; import {tsBuildTask, copyTask, buildAppTask, serverTask} from '../util/task_helpers'; import {join} from 'path'; import { - buildConfig, copyFiles, buildScssTask, triggerLivereload, sequenceTask + buildConfig, copyFiles, buildScssTask, triggerLivereload, sequenceTask, watchFiles } from 'lib-build-tools'; // These imports don't have any typings provided. @@ -27,12 +27,12 @@ const appVendors = [ const vendorGlob = `+(${appVendors.join('|')})/**/*.+(html|css|js|map)`; task(':watch:devapp', () => { - watch(join(appDir, '**/*.ts'), [':build:devapp:ts', triggerLivereload]); - watch(join(appDir, '**/*.scss'), [':build:devapp:scss', triggerLivereload]); - watch(join(appDir, '**/*.html'), [':build:devapp:assets', triggerLivereload]); - watch(join(appDir, '**/*.css'), [':build:devapp:assets', triggerLivereload]); + watchFiles(join(appDir, '**/*.ts'), [':build:devapp:ts']); + watchFiles(join(appDir, '**/*.scss'), [':build:devapp:scss']); + watchFiles(join(appDir, '**/*.html'), [':build:devapp:assets']); + watchFiles(join(appDir, '**/*.css'), [':build:devapp:assets']); - watch(join(libOutPath, '**/*.css'), [':build:devapp:scss', triggerLivereload]); + watchFiles(join(libOutPath, '**/*.css'), [':build:devapp:scss']); }); /** Path to the demo-app tsconfig file. */ diff --git a/tools/gulp/tasks/lint.ts b/tools/gulp/tasks/lint.ts index 53f43a162..31044d479 100644 --- a/tools/gulp/tasks/lint.ts +++ b/tools/gulp/tasks/lint.ts @@ -2,6 +2,10 @@ import {task} from 'gulp'; import {execNodeTask} from '../util/task_helpers'; import {join} from 'path'; import {buildConfig} from 'lib-build-tools'; +import {red} from 'chalk'; + +// These types lack of type definitions +const madge = require('madge'); /** Glob that matches all SCSS or CSS files that should be linted. */ const stylesGlob = '+(tools|src)/**/!(*.bundle).+(css|scss)'; @@ -17,10 +21,7 @@ const cdkOutPath = join(buildConfig.outputDir, 'packages', 'cdk'); task('lint', ['tslint', 'stylelint', 'madge']); -/** Task that runs madge to detect circular dependencies. */ -task('madge', ['flex-layout:clean-build'], execNodeTask( - 'madge', ['--circular', libOutPath]) -); + /** Task to lint Angular Flex-Layout's scss stylesheets. */ task('stylelint', execNodeTask( @@ -32,3 +33,22 @@ task('tslint', execNodeTask('tslint', tsLintBaseFlags)); /** Task that automatically fixes TSLint warnings. */ task('tslint:fix', execNodeTask('tslint', [...tsLintBaseFlags, '--fix'])); + +/** Task that runs madge to detect circular dependencies. */ +task('madge', ['flex-layout:clean-build'], () => { + madge([libOutPath]).then((res: any) => { + const circularModules = res.circular(); + + if (circularModules.length) { + console.error(); + console.error(red(`Madge found modules with circular dependencies.`)); + console.error(formatMadgeCircularModules(circularModules)); + console.error(); + } + }); +}); + +/** Returns a string that formats the graph of circular modules. */ +function formatMadgeCircularModules(circularModules: string[][]): string { + return circularModules.map((modulePaths: string[]) => `\n - ${modulePaths.join(' > ')}`).join(''); +} diff --git a/tools/gulp/tsconfig.json b/tools/gulp/tsconfig.json index 059094cf0..c9f6a32c5 100644 --- a/tools/gulp/tsconfig.json +++ b/tools/gulp/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "experimentalDecorators": true, "noUnusedParameters": true, - "lib": ["es2015", "dom"], + "lib": ["es2015", "dom", "es2016.array.include"], "module": "commonjs", "moduleResolution": "node", "outDir": "../../dist/tools/gulp", diff --git a/tools/package-tools/build-bundles.ts b/tools/package-tools/build-bundles.ts index 61c381fc1..ab84af913 100644 --- a/tools/package-tools/build-bundles.ts +++ b/tools/package-tools/build-bundles.ts @@ -9,28 +9,49 @@ import {buildConfig} from './build-config'; /** Directory where all bundles will be created in. */ const bundlesDir = join(buildConfig.outputDir, 'bundles'); -/** Builds the bundles for the specified package. */ -export async function buildPackageBundles(entryFile: string, packageName: string) { - const moduleName = `ng.${packageName}`; +/** Builds bundles for the primary entry-point w/ given entry file, e.g. @angular/cdk */ +export async function buildPrimaryEntryPointBundles(entryFile: string, packageName: string) { + return createBundlesForEntryPoint({ + entryFile, + moduleName: `ng.${packageName}`, + fesm2015Dest: join(bundlesDir, `${packageName}.js`), + fesm2014Dest: join(bundlesDir, `${packageName}.es5.js`), + umdDest: join(bundlesDir, `${packageName}.umd.js`), + umdMinDest: join(bundlesDir, `${packageName}.umd.min.js`), + }); +} - // List of paths to the package bundles. - const fesm2015File = join(bundlesDir, `${packageName}.js`); - const fesm2014File = join(bundlesDir, `${packageName}.es5.js`); - const umdFile = join(bundlesDir, `${packageName}.umd.js`); - const umdMinFile = join(bundlesDir, `${packageName}.umd.min.js`); +/** Builds bundles for a single secondary entry-point w/ given entry file, e.g. @angular/cdk/a11y */ +export async function buildSecondaryEntryPointBundles(entryFile: string, packageName: string, + entryPointName: string) { + return createBundlesForEntryPoint({ + entryFile, + moduleName: `ng.${packageName}.${entryPointName}`, + fesm2015Dest: join(bundlesDir, `${packageName}`, `${entryPointName}.js`), + fesm2014Dest: join(bundlesDir, `${packageName}`, `${entryPointName}.es5.js`), + umdDest: join(bundlesDir, `${packageName}-${entryPointName}.umd.js`), + umdMinDest: join(bundlesDir, `${packageName}-${entryPointName}.umd.min.js`), + }); +} +/** + * Creates the ES5, ES2015, and UMD bundles for the specified entry-point. + * @param config Configuration that specifies the entry-point, module name, and output + * bundle paths. + */ +async function createBundlesForEntryPoint(config: BundlesConfig) { // Build FESM-2015 bundle file. await createRollupBundle({ - moduleName: moduleName, - entry: entryFile, - dest: fesm2015File, + moduleName: config.moduleName, + entry: config.entryFile, + dest: config.fesm2015Dest, format: 'es', }); - await remapSourcemap(fesm2015File); + await remapSourcemap(config.fesm2015Dest); // Downlevel FESM-2015 file to ES5. - transpileFile(fesm2015File, fesm2014File, { + transpileFile(config.fesm2015Dest, config.fesm2014Dest, { importHelpers: true, target: ScriptTarget.ES5, module: ModuleKind.ES2015, @@ -38,20 +59,31 @@ export async function buildPackageBundles(entryFile: string, packageName: string newLine: NewLineKind.LineFeed }); - await remapSourcemap(fesm2014File); + await remapSourcemap(config.fesm2014Dest); // Create UMD bundle of FESM-2014 output. await createRollupBundle({ - moduleName: moduleName, - entry: fesm2014File, - dest: umdFile, + moduleName: config.moduleName, + entry: config.fesm2014Dest, + dest: config.umdDest, format: 'umd' }); - await remapSourcemap(umdFile); + await remapSourcemap(config.umdDest); // Create a minified UMD bundle using UglifyJS - uglifyJsFile(umdFile, umdMinFile); + uglifyJsFile(config.umdDest, config.umdMinDest); + + await remapSourcemap(config.umdMinDest); +} + - await remapSourcemap(umdMinFile); +/** Configuration for creating library bundles. */ +interface BundlesConfig { + entryFile: string; + moduleName: string; + fesm2015Dest: string; + fesm2014Dest: string; + umdDest: string; + umdMinDest: string; } diff --git a/tools/package-tools/build-package.ts b/tools/package-tools/build-package.ts new file mode 100644 index 000000000..d90af0899 --- /dev/null +++ b/tools/package-tools/build-package.ts @@ -0,0 +1,86 @@ +import {join} from 'path'; +import {main as tsc} from '@angular/tsc-wrapped'; +import {buildConfig} from './build-config'; +import {getSecondaryEntryPointsForPackage} from './secondary-entry-points'; +import {buildPrimaryEntryPointBundles, buildSecondaryEntryPointBundles} from './build-bundles'; + + +const {packagesDir, outputDir} = buildConfig; + +/** Name of the tsconfig file that is responsible for building a package. */ +const buildTsconfigName = 'tsconfig-build.json'; + +/** Name of the tsconfig file that is responsible for building the tests. */ +const testsTsconfigName = 'tsconfig-tests.json'; + +export class BuildPackage { + + /** Path to the package sources. */ + packageRoot: string; + + /** Path to the package output. */ + packageOut: string; + + /** Path to the entry file of the package in the output directory. */ + private entryFilePath: string; + + /** Path to the tsconfig file, which will be used to build the package. */ + private tsconfigBuild: string; + + /** Path to the tsconfig file, which will be used to build the tests. */ + private tsconfigTests: string; + + /** Secondary entry points for the package. */ + get secondaryEntryPoints(): string[] { + if (!this._secondaryEntryPoints) { + this._secondaryEntryPoints = getSecondaryEntryPointsForPackage(this); + } + + return this._secondaryEntryPoints; + } + + private _secondaryEntryPoints: string[]; + + constructor(public packageName: string, public dependencies: BuildPackage[] = []) { + this.packageRoot = join(packagesDir, packageName); + this.packageOut = join(outputDir, 'packages', packageName); + + this.tsconfigBuild = join(this.packageRoot, buildTsconfigName); + this.tsconfigTests = join(this.packageRoot, testsTsconfigName); + + this.entryFilePath = join(this.packageOut, 'index.js'); + } + + /** Compiles the package sources with all secondary entry points. */ + async compile() { + await this._compileEntryPoint(buildTsconfigName); + + // Walk through every secondary entry point and build the TypeScript sources sequentially. + for (const entryPoint of this.secondaryEntryPoints) { + await this._compileEntryPoint(buildTsconfigName, entryPoint); + } + } + + /** Compiles the TypeScript test source files for the package. */ + async compileTests() { + await this._compileEntryPoint(testsTsconfigName); + } + + /** Creates all bundles for the package and all associated entry points. */ + async createBundles() { + await buildPrimaryEntryPointBundles(this.entryFilePath, this.packageName); + + for (const entryPoint of this.secondaryEntryPoints) { + const entryPointEntryFilePath = join(this.packageOut, entryPoint, 'index.js'); + await buildSecondaryEntryPointBundles(entryPointEntryFilePath, this.packageName, entryPoint); + } + } + + /** Compiles the TypeScript sources of a primary or secondary entry point. */ + private async _compileEntryPoint(tsconfigName: string, secondaryEntryPoint?: string) { + const entryPointPath = join(this.packageRoot, secondaryEntryPoint || ''); + const entryPointTsconfigPath = join(entryPointPath, tsconfigName); + + await tsc(entryPointTsconfigPath, {basePath: entryPointPath}); + } +} diff --git a/tools/package-tools/build-release.ts b/tools/package-tools/build-release.ts index 4d5c63ea6..c9a3c9fdb 100644 --- a/tools/package-tools/build-release.ts +++ b/tools/package-tools/build-release.ts @@ -1,11 +1,14 @@ import {join} from 'path'; +import {mkdirpSync} from 'fs-extra'; import {copyFiles} from './copy-files'; -import {addPureAnnotationsToFile} from './pure-annotations'; -import {updatePackageVersion} from './package-versions'; +import {replaceVersionPlaceholders} from './version-placeholders'; import {inlinePackageMetadataFiles} from './metadata-inlining'; import {createTypingsReexportFile} from './typings-reexport'; import {createMetadataReexportFile} from './metadata-reexport'; +import {getSecondaryEntryPointsForPackage} from './secondary-entry-points'; +import {createEntryPointPackageJson} from './entry-point-package-json'; import {buildConfig} from './build-config'; +import {BuildPackage} from './build-package'; const {packagesDir, outputDir, projectDir} = buildConfig; @@ -17,23 +20,57 @@ const bundlesDir = join(outputDir, 'bundles'); * release folder structure. The output will also contain a README and the according package.json * file. Additionally the package will be Closure Compiler and AOT compatible. */ -export function composeRelease(packageName: string) { - // To avoid refactoring of the project the package 'flex-layout' will map to the source path `lib/`. - const sourcePath = join(packagesDir, packageName === 'flex-layout' ? 'lib' : packageName); - const packagePath = join(outputDir, 'packages', packageName); +export function composeRelease(buildPackage: BuildPackage) { + const {packageName, packageOut, packageRoot} = buildPackage; const releasePath = join(outputDir, 'releases', packageName); - inlinePackageMetadataFiles(packagePath); + inlinePackageMetadataFiles(packageOut); - copyFiles(packagePath, '**/*.+(d.ts|metadata.json)', join(releasePath, 'typings')); - copyFiles(bundlesDir, `${packageName}.umd?(.min).js?(.map)`, join(releasePath, 'bundles')); + copyFiles(packageOut, '**/*.+(d.ts|metadata.json)', join(releasePath, 'typings')); + copyFiles(bundlesDir, `${packageName}?(-*).umd.js?(.map)`, join(releasePath, 'bundles')); copyFiles(bundlesDir, `${packageName}?(.es5).js?(.map)`, join(releasePath, '@angular')); + copyFiles(join(bundlesDir, packageName), '**', join(releasePath, '@angular', packageName)); copyFiles(projectDir, 'LICENSE', releasePath); copyFiles(packagesDir, 'README.md', releasePath); - copyFiles(sourcePath, 'package.json', releasePath); + copyFiles(packageRoot, 'package.json', releasePath); - updatePackageVersion(releasePath); - createTypingsReexportFile(releasePath, packageName); - createMetadataReexportFile(releasePath, packageName); - addPureAnnotationsToFile(join(releasePath, '@angular', `${packageName}.es5.js`)); + replaceVersionPlaceholders(releasePath); + createTypingsReexportFile(releasePath, './typings/index', packageName); + createMetadataReexportFile(releasePath, './typings/index', packageName); + + if (buildPackage.secondaryEntryPoints.length) { + createFilesForSecondaryEntryPoint(buildPackage, releasePath); + } +} + +/** Creates files necessary for a secondary entry-point. */ +function createFilesForSecondaryEntryPoint(buildPackage: BuildPackage, releasePath: string) { + const {packageName, packageOut} = buildPackage; + + getSecondaryEntryPointsForPackage(buildPackage).forEach(entryPointName => { + // Create a directory in the root of the package for this entry point that contains + // * A package.json that lists the different bundle locations + // * An index.d.ts file that re-exports the index.d.ts from the typings/ directory + // * A metadata.json re-export for this entry-point's metadata. + const entryPointDir = join(releasePath, entryPointName); + + mkdirpSync(entryPointDir); + createEntryPointPackageJson(entryPointDir, packageName, entryPointName); + + // Copy typings and metadata from tsc output location into the entry-point. + copyFiles( + join(packageOut, entryPointName), + '**/*.+(d.ts|metadata.json)', + join(entryPointDir, 'typings')); + + // Create a typings and a metadata re-export within the entry-point to point to the + // typings we just copied. + createTypingsReexportFile(entryPointDir, `./typings/index`, 'index'); + createMetadataReexportFile(entryPointDir, `./typings/index`, 'index'); + + // Finally, create both a d.ts and metadata file for this entry-point in the root of + // the package that re-exports from the entry-point's directory. + createTypingsReexportFile(releasePath, `./${entryPointName}/index`, entryPointName); + createMetadataReexportFile(releasePath, `./${entryPointName}/index`, entryPointName); + }); } diff --git a/tools/package-tools/copy-files.ts b/tools/package-tools/copy-files.ts index 48412d3ef..c9baf5fe0 100644 --- a/tools/package-tools/copy-files.ts +++ b/tools/package-tools/copy-files.ts @@ -5,7 +5,7 @@ import {join, dirname} from 'path'; /** Function to copy files that match a glob to another directory. */ export function copyFiles(fromPath: string, fileGlob: string, outDir: string) { glob(fileGlob, {cwd: fromPath}).forEach(filePath => { - let fileDestPath = join(outDir, filePath); + const fileDestPath = join(outDir, filePath); mkdirpSync(dirname(fileDestPath)); copySync(join(fromPath, filePath), fileDestPath); }); diff --git a/tools/package-tools/entry-point-package-json.ts b/tools/package-tools/entry-point-package-json.ts new file mode 100644 index 000000000..f804be617 --- /dev/null +++ b/tools/package-tools/entry-point-package-json.ts @@ -0,0 +1,16 @@ +import {join} from 'path'; +import {writeFileSync} from 'fs'; + +/** Creates a package.json for a secondary entry-point with the different bundle locations. */ +export function createEntryPointPackageJson(destDir: string, packageName: string, + entryPointName: string) { + const content = { + name: `@angular/${packageName}/${entryPointName}`, + typings: `../${entryPointName}.d.ts`, + main: `../bundles/${packageName}-${entryPointName}.umd.js`, + module: `../@angular/${packageName}/${entryPointName}.es5.js`, + es2015: `../@angular/${packageName}/${entryPointName}.js`, + }; + + writeFileSync(join(destDir, 'package.json'), JSON.stringify(content, null, 2), 'utf-8'); +} diff --git a/tools/package-tools/gulp/build-tasks-gulp.ts b/tools/package-tools/gulp/build-tasks-gulp.ts index e4adede4c..11edecc74 100644 --- a/tools/package-tools/gulp/build-tasks-gulp.ts +++ b/tools/package-tools/gulp/build-tasks-gulp.ts @@ -1,19 +1,15 @@ -import {task, watch, src, dest} from 'gulp'; +import {dest, src, task} from 'gulp'; import {join} from 'path'; -import {main as tsc} from '@angular/tsc-wrapped'; -import {buildConfig} from '../build-config'; import {composeRelease} from '../build-release'; -import {buildPackageBundles} from '../build-bundles'; import {inlineResourcesForDirectory} from '../inline-resources'; import {buildScssTask} from './build-scss-task'; import {sequenceTask} from './sequence-task'; -import {triggerLivereload} from './trigger-livereload'; +import {watchFiles} from './watch-files'; +import {BuildPackage} from '../build-package'; // There are no type definitions available for these imports. const htmlmin = require('gulp-htmlmin'); -const {packagesDir, outputDir} = buildConfig; - const htmlMinifierOptions = { collapseWhitespace: true, removeComments: true, @@ -21,97 +17,91 @@ const htmlMinifierOptions = { removeAttributeQuotes: false }; -/** - * Creates a set of gulp tasks that can build the specified package. - * @param packageName Name of the package. Needs to be similar to the directory name in `src/`. - * @param requiredPackages Required packages that will be built before building the current package. - */ -export function createPackageBuildTasks(packageName: string, requiredPackages: string[] = []) { - - // To avoid refactoring of the project the package src/lib will map to the source path `lib/`. - const packageRoot = join(packagesDir, packageName === 'flex-layout' ? 'lib' : packageName); - const packageOut = join(outputDir, 'packages', packageName); +/** Creates a set of gulp tasks that can build the specified package. */ +export function createPackageBuildTasks(buildPackage: BuildPackage) { + // Name of the package build tasks for Gulp. + const taskName = buildPackage.packageName; - const tsconfigBuild = join(packageRoot, 'tsconfig-build.json'); - const tsconfigTests = join(packageRoot, 'tsconfig-tests.json'); - - // Paths to the different output files and directories. - const esmMainFile = join(packageOut, 'index.js'); + // Name of all dependencies of the current package. + const dependencyNames = buildPackage.dependencies.map(p => p.packageName); // Glob that matches all style files that need to be copied to the package output. - const stylesGlob = join(packageRoot, '**/*.+(scss|css)'); + const stylesGlob = join(buildPackage.packageRoot, '**/*.+(scss|css)'); // Glob that matches every HTML file in the current package. - const htmlGlob = join(packageRoot, '**/*.html'); + const htmlGlob = join(buildPackage.packageRoot, '**/*.html'); // List of watch tasks that need run together with the watch task of the current package. - const dependentWatchTasks = requiredPackages.map(pkgName => `${pkgName}:watch`); + const dependentWatchTasks = buildPackage.dependencies.map(p => `${p.packageName}:watch`); /** * Main tasks for the package building. Tasks execute the different sub-tasks in the correct * order. */ - task(`${packageName}:clean-build`, sequenceTask('clean', `${packageName}:build`)); + task(`${taskName}:clean-build`, sequenceTask('clean', `${taskName}:build`)); - task(`${packageName}:build`, sequenceTask( + task(`${taskName}:build`, sequenceTask( // Build all required packages before building. - ...requiredPackages.map(pkgName => `${pkgName}:build`), + ...dependencyNames.map(pkgName => `${pkgName}:build`), // Build ESM and assets output. - [`${packageName}:build:esm`, `${packageName}:assets`], + [`${taskName}:build:esm`, `${taskName}:assets`], // Inline assets into ESM output. - `${packageName}:assets:inline`, + `${taskName}:assets:inline`, // Build bundles on top of inlined ESM output. - `${packageName}:build:bundles`, + `${taskName}:build:bundles`, )); - task(`${packageName}:build-tests`, sequenceTask( + task(`${taskName}:build-tests`, sequenceTask( // Build all required tests before building. - ...requiredPackages.map(pkgName => `${pkgName}:build-tests`), + ...dependencyNames.map(pkgName => `${pkgName}:build-tests`), // Build the ESM output that includes all test files. Also build assets for the package. - [`${packageName}:build:esm:tests`, `${packageName}:assets`], + [`${taskName}:build:esm:tests`, `${taskName}:assets`], // Inline assets into ESM output. - `${packageName}:assets:inline` + `${taskName}:assets:inline` )); /** * Release tasks for the package. Tasks compose the release output for the package. */ - task(`${packageName}:build-release:clean`, sequenceTask('clean', `${packageName}:build-release`)); - task(`${packageName}:build-release`, [`${packageName}:build`], () => composeRelease(packageName)); + + task(`${taskName}:build-release:clean`, sequenceTask('clean', `${taskName}:build-release`)); + task(`${taskName}:build-release`, [`${taskName}:build`], () => composeRelease(buildPackage)); + /** * TypeScript compilation tasks. Tasks are creating ESM, FESM, UMD bundles for releases. */ - task(`${packageName}:build:esm`, () => tsc(tsconfigBuild, {basePath: packageRoot})); - - task(`force:build`, () => tsc(tsconfigBuild, {basePath: packageRoot})); - task(`${packageName}:build:esm:tests`, () => tsc(tsconfigTests, {basePath: packageRoot})); + task(`${taskName}:build:esm`, () => buildPackage.compile()); + task(`${taskName}:build:esm:tests`, () => buildPackage.compileTests()); - task(`${packageName}:build:bundles`, () => buildPackageBundles(esmMainFile, packageName)); + task(`${taskName}:build:bundles`, () => buildPackage.createBundles()); /** * Asset tasks. Building SASS files and inlining CSS, HTML files into the ESM output. */ - task(`${packageName}:assets`, [ - `${packageName}:assets:scss`, - `${packageName}:assets:copy-styles`, - `${packageName}:assets:html` + task(`${taskName}:assets`, [ + `${taskName}:assets:scss`, + `${taskName}:assets:copy-styles`, + `${taskName}:assets:html` ]); - task(`${packageName}:assets:scss`, buildScssTask(packageOut, packageRoot, true)); - task(`${packageName}:assets:copy-styles`, () => { - return src(stylesGlob).pipe(dest(packageOut)); + task(`${taskName}:assets:scss`, buildScssTask( + buildPackage.packageOut, buildPackage.packageRoot, true) + ); + + task(`${taskName}:assets:copy-styles`, () => { + return src(stylesGlob).pipe(dest(buildPackage.packageOut)); }); - task(`${packageName}:assets:html`, () => { - return src(htmlGlob).pipe(htmlmin(htmlMinifierOptions)).pipe(dest(packageOut)); + task(`${taskName}:assets:html`, () => { + return src(htmlGlob).pipe(htmlmin(htmlMinifierOptions)).pipe(dest(buildPackage.packageOut)); }); - task(`${packageName}:assets:inline`, () => inlineResourcesForDirectory(packageOut)); + task(`${taskName}:assets:inline`, () => inlineResourcesForDirectory(buildPackage.packageOut)); /** * Watch tasks, that will rebuild the package whenever TS, SCSS, or HTML files change. */ - task(`${packageName}:watch`, dependentWatchTasks, () => { - watch(join(packageRoot, '**/*.+(ts|scss|html)'), [`${packageName}:build`, triggerLivereload]); + task(`${taskName}:watch`, dependentWatchTasks, () => { + watchFiles(join(buildPackage.packageRoot, '**/*.+(ts|scss|html)'), [`${taskName}:build`]); }); } diff --git a/tools/package-tools/gulp/watch-files.ts b/tools/package-tools/gulp/watch-files.ts new file mode 100644 index 000000000..174badf9c --- /dev/null +++ b/tools/package-tools/gulp/watch-files.ts @@ -0,0 +1,13 @@ +import {watch} from 'gulp'; +import {triggerLivereload} from './trigger-livereload'; + +/** Options that will be passed to the watch function of Gulp.*/ +const gulpWatchOptions = { debounceDelay: 700 }; + +/** + * Function that watches a set of file globs and runs given Gulp tasks if a given file changes. + * By default the livereload server will be also called on file change. + */ +export function watchFiles(fileGlob: string | string[], tasks: string[], livereload = true) { + watch(fileGlob, gulpWatchOptions, [...tasks, () => livereload && triggerLivereload()]); +} diff --git a/tools/package-tools/index.ts b/tools/package-tools/index.ts index 60b77fdcb..dec848ca5 100644 --- a/tools/package-tools/index.ts +++ b/tools/package-tools/index.ts @@ -2,6 +2,7 @@ export * from './build-config'; export * from './build-bundles'; export * from './build-release'; +export * from './build-package'; export * from './copy-files'; // Expose gulp utilities. @@ -9,3 +10,4 @@ export * from './gulp/build-tasks-gulp'; export * from './gulp/build-scss-task'; export * from './gulp/sequence-task'; export * from './gulp/trigger-livereload'; +export * from './gulp/watch-files'; diff --git a/tools/package-tools/metadata-reexport.ts b/tools/package-tools/metadata-reexport.ts index 3348227f9..cd79e86c6 100644 --- a/tools/package-tools/metadata-reexport.ts +++ b/tools/package-tools/metadata-reexport.ts @@ -2,12 +2,14 @@ import {writeFileSync} from 'fs'; import {join} from 'path'; /** Creates a metadata file that re-exports the metadata bundle inside of the typings. */ -export function createMetadataReexportFile(packageDir: string, packageName: string) { - const metadataReExport = `{ - "__symbolic":"module", - "version":3,"metadata":{}, - "exports":[{"from":"./typings/index"}], - "flatModuleIndexRedirect": true - }`; - writeFileSync(join(packageDir, `${packageName}.metadata.json`), metadataReExport, 'utf-8'); +export function createMetadataReexportFile(destDir: string, from: string, name: string) { + const metadataJsonContent = JSON.stringify({ + __symbolic: 'module', + version: 3, + metadata: {}, + exports: [{from}], + flatModuleIndexRedirect: true + }, null, 2); + + writeFileSync(join(destDir, `${name}.metadata.json`), metadataJsonContent, 'utf-8'); } diff --git a/tools/package-tools/package-versions.ts b/tools/package-tools/package-versions.ts deleted file mode 100644 index 110297e59..000000000 --- a/tools/package-tools/package-versions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {writeFileSync} from 'fs'; -import {join} from 'path'; -import {buildConfig} from './build-config'; - -/** Version of the project that will be used to replace the placeholder. */ -const {projectVersion} = buildConfig; - -/** Updates the `package.json` file of the specified package. Replaces the version placeholder. */ -export function updatePackageVersion(packageDir: string) { - const packagePath = join(packageDir, 'package.json'); - const packageConfig = require(packagePath); - - // Replace the `0.0.0-PLACEHOLDER` version name with the version of the root package.json file. - packageConfig.version = packageConfig.version.replace('0.0.0-PLACEHOLDER', projectVersion); - - writeFileSync(packagePath, JSON.stringify(packageConfig, null, 2)); -} diff --git a/tools/package-tools/pure-annotations.ts b/tools/package-tools/pure-annotations.ts deleted file mode 100644 index 9fa971e4c..000000000 --- a/tools/package-tools/pure-annotations.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {readFileSync, writeFileSync} from 'fs'; - -/** Regex that matches downleveled class IIFE expressions. Used to add the pure annotations. */ -const classIfeeRegex = - new RegExp('^(var (\\S+) = )(\\(function \\(\\) \\{[\\n\\r]*(?: (?:\\/\\*\\*| \\*|\\*\\/|' + - '\\/\\/)[^\\n\\r]*[\\n\\r]*)* function \\2\\([^\\)]*\\) \\{[\\n\\r]*)', 'mg'); - -/** Regex that matches downleveled class IIFE expressions with _extends statements */ -const classExtendsIfeeRegex = - /^(var (\S+) = )(\(function \(_super\) {[\n\r]* tslib.*\.__extends\(\2, _super\);[\n\r]*)/gm; - -/** - * Adds `@__PURE__` annotation comments to IIFEs containing ES5-downleveled classes generated by - * TypeScript so that Uglify can tree-shake classes that are not referenced. - * - * @param fileContent The content of the file for which `@__PURE__` will be added. - * @returns The content of the file with `@__PURE__` annotations added. - */ -export function addPureAnnotations(fileContent: string) { - return fileContent - // Prefix downleveled classes w/ the @__PURE__ annotation. - .replace(classIfeeRegex, '$1/*@__PURE__*/$3') - // Prefix downleveled classes that extend another class w/ the @__PURE__ annotation - .replace(classExtendsIfeeRegex, '$1/*@__PURE__*/$3'); -} - -/** Adds Uglify "@__PURE__" decorations to the specified file. */ -export function addPureAnnotationsToFile(inputFile: string) { - const originalContent = readFileSync(inputFile, 'utf-8'); - const annotatedContent = addPureAnnotations(originalContent); - - writeFileSync(inputFile, annotatedContent, 'utf-8'); -} diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts new file mode 100644 index 000000000..ccc690b0c --- /dev/null +++ b/tools/package-tools/rollup-globals.ts @@ -0,0 +1,70 @@ +import {join} from 'path'; +import {getSubdirectoryNames} from './secondary-entry-points'; +import {buildConfig} from './build-config'; + +/** Method that converts dash-case strings to a camel-based string. */ +const dashCaseToCamelCase = (str: string) => str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); + +/** Map of globals that are used inside of the different packages. */ +export const rollupGlobals = { + 'tslib': 'tslib', + + '@angular/animations': 'ng.animations', + '@angular/core': 'ng.core', + '@angular/common': 'ng.common', + '@angular/forms': 'ng.forms', + '@angular/http': 'ng.http', + '@angular/router': 'ng.router', + '@angular/platform-browser': 'ng.platformBrowser', + '@angular/platform-server': 'ng.platformServer', + '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', + '@angular/platform-browser/animations': 'ng.platformBrowser.animations', + '@angular/core/testing': 'ng.core.testing', + '@angular/common/testing': 'ng.common.testing', + '@angular/http/testing': 'ng.http.testing', + + '@angular/flex-layout': 'ng.flex-layout', + + // Some packages are not really needed for the UMD bundles, but for the missingRollupGlobals rule. + // TODO(devversion): remove by adding minimatch and better globbing to rules + + 'rxjs/BehaviorSubject': 'Rx', + 'rxjs/Observable': 'Rx', + 'rxjs/Subject': 'Rx', + 'rxjs/Subscription': 'Rx', + 'rxjs/Observer': 'Rx', + 'rxjs/Scheduler': 'Rx', + 'rxjs/observable/combineLatest': 'Rx.Observable', + 'rxjs/observable/forkJoin': 'Rx.Observable', + 'rxjs/observable/fromEvent': 'Rx.Observable', + 'rxjs/observable/merge': 'Rx.Observable', + 'rxjs/observable/of': 'Rx.Observable', + 'rxjs/observable/throw': 'Rx.Observable', + 'rxjs/observable/defer': 'Rx.Observable', + 'rxjs/operator/auditTime': 'Rx.Observable.prototype', + 'rxjs/operator/catch': 'Rx.Observable.prototype', + 'rxjs/operator/debounceTime': 'Rx.Observable.prototype', + 'rxjs/operator/do': 'Rx.Observable.prototype', + 'rxjs/operator/filter': 'Rx.Observable.prototype', + 'rxjs/operator/finally': 'Rx.Observable.prototype', + 'rxjs/operator/first': 'Rx.Observable.prototype', + 'rxjs/operator/let': 'Rx.Observable.prototype', + 'rxjs/operator/map': 'Rx.Observable.prototype', + 'rxjs/operator/share': 'Rx.Observable.prototype', + 'rxjs/operator/startWith': 'Rx.Observable.prototype', + 'rxjs/operator/switchMap': 'Rx.Observable.prototype', + 'rxjs/operator/takeUntil': 'Rx.Observable.prototype', + 'rxjs/operator/toPromise': 'Rx.Observable.prototype', + + 'rxjs/add/observable/merge': 'Rx.Observable', + 'rxjs/add/observable/fromEvent': 'Rx.Observable', + 'rxjs/add/observable/of': 'Rx.Observable', + 'rxjs/add/observable/interval': 'Rx.Observable', + 'rxjs/add/operator/startWith': 'Rx.Observable.prototype', + 'rxjs/add/operator/map': 'Rx.Observable.prototype', + 'rxjs/add/operator/debounceTime': 'Rx.Observable.prototype', + 'rxjs/add/operator/distinctUntilChanged': 'Rx.Observable.prototype', + 'rxjs/add/operator/first': 'Rx.Observable.prototype', + 'rxjs/add/operator/catch': 'Rx.Observable.prototype', + 'rxjs/add/operator/switchMap': 'Rx.Observable.prototype' +}; diff --git a/tools/package-tools/rollup-helpers.ts b/tools/package-tools/rollup-helpers.ts index 9f7ee7972..8cdd4fbb6 100644 --- a/tools/package-tools/rollup-helpers.ts +++ b/tools/package-tools/rollup-helpers.ts @@ -1,59 +1,11 @@ import {buildConfig} from './build-config'; import {rollupRemoveLicensesPlugin} from './rollup-remove-licenses'; +import {rollupGlobals} from './rollup-globals'; // There are no type definitions available for these imports. const rollup = require('rollup'); const rollupNodeResolutionPlugin = require('rollup-plugin-node-resolve'); -const ROLLUP_GLOBALS = { - // Import tslib rather than having TypeScript output its helpers multiple times. - // See https://github.com/Microsoft/tslib - 'tslib': 'tslib', - - // Angular dependencies - '@angular/animations': 'ng.animations', - '@angular/core': 'ng.core', - '@angular/common': 'ng.common', - '@angular/forms': 'ng.forms', - '@angular/platform-browser': 'ng.platformBrowser', - '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', - '@angular/platform-browser/animations': 'ng.platformBrowser.animations', - '@angular/material': 'ng.material', - '@angular/cdk': 'ng.cdk', - - // Local Angular packages inside of Flex-Layout. - '@angular/flex-layout': 'ng.flexLayout', - - // RxJS dependencies - 'rxjs/BehaviorSubject': 'Rx', - 'rxjs/Observable': 'Rx', - 'rxjs/Subject': 'Rx', - 'rxjs/Subscription': 'Rx', - 'rxjs/observable/combineLatest': 'Rx.Observable', - 'rxjs/observable/forkJoin': 'Rx.Observable', - 'rxjs/observable/fromEvent': 'Rx.Observable', - 'rxjs/observable/merge': 'Rx.Observable', - 'rxjs/observable/of': 'Rx.Observable', - 'rxjs/observable/throw': 'Rx.Observable', - 'rxjs/operator/catch': 'Rx.Observable.prototype', - 'rxjs/operator/debounceTime': 'Rx.Observable.prototype', - 'rxjs/operator/do': 'Rx.Observable.prototype', - 'rxjs/operator/filter': 'Rx.Observable.prototype', - 'rxjs/operator/finally': 'Rx.Observable.prototype', - 'rxjs/operator/let': 'Rx.Observable.prototype', - 'rxjs/operator/map': 'Rx.Observable.prototype', - 'rxjs/operator/startWith': 'Rx.Observable.prototype', - 'rxjs/operator/switchMap': 'Rx.Observable.prototype', - 'rxjs/operator/takeUntil': 'Rx.Observable.prototype', - 'rxjs/operator/toPromise': 'Rx.Observable.prototype', - - // RxJS imports for the examples package - 'rxjs/add/observable/fromEvent': 'Rx.Observable', - 'rxjs/add/operator/takeUntil': 'Rx.Observable.prototype', - 'rxjs/add/operator/switchMap': 'Rx.Observable.prototype', - 'rxjs/add/operator/map': 'Rx.Observable.prototype' -}; - export type BundleConfig = { entry: string; dest: string; @@ -65,7 +17,7 @@ export type BundleConfig = { export function createRollupBundle(config: BundleConfig): Promise { const bundleOptions = { context: 'this', - external: Object.keys(ROLLUP_GLOBALS), + external: Object.keys(rollupGlobals), entry: config.entry, plugins: [rollupRemoveLicensesPlugin] }; @@ -77,7 +29,7 @@ export function createRollupBundle(config: BundleConfig): Promise { banner: buildConfig.licenseBanner, format: config.format, dest: config.dest, - globals: ROLLUP_GLOBALS, + globals: rollupGlobals, sourceMap: true }; @@ -86,7 +38,7 @@ export function createRollupBundle(config: BundleConfig): Promise { if (config.format === 'umd') { bundleOptions.plugins.push(rollupNodeResolutionPlugin()); - const external = Object.keys(ROLLUP_GLOBALS); + const external = Object.keys(rollupGlobals); external.splice(external.indexOf('tslib'), 1); bundleOptions.external = external; } diff --git a/tools/package-tools/secondary-entry-points.ts b/tools/package-tools/secondary-entry-points.ts new file mode 100644 index 000000000..54989974a --- /dev/null +++ b/tools/package-tools/secondary-entry-points.ts @@ -0,0 +1,88 @@ +import {join} from 'path'; +import {readdirSync, lstatSync, existsSync} from 'fs'; +import {spawnSync} from 'child_process'; +import {BuildPackage} from './build-package'; + + +/** + * Gets secondary entry-points for a given package in the order they should be built. + * + * This currently assumes that every directory under a package should be an entry-point except for + * specifically black-listed directories. + * + * @param pkg The package for which to get entry points, e.g., 'cdk'. + * @returns An array of secondary entry-points names, e.g., ['a11y', 'bidi', ...] + */ +export function getSecondaryEntryPointsForPackage(pkg: BuildPackage) { + const packageName = pkg.packageName; + const packageDir = pkg.packageRoot; + + // Get the list of all entry-points as the list of directories in the package that have a + // tsconfig-build.json + const entryPoints = getSubdirectoryNames(packageDir) + .filter(d => existsSync(join(packageDir, d, 'tsconfig-build.json'))); + + // Create nodes that comprise the build graph. + const buildNodes: BuildNode[] = entryPoints.map(p => ({name: p, deps: []})); + + // Create a lookup for name -> build graph node. + const nodeLookup = buildNodes.reduce((lookup, node) => { + return lookup.set(node.name, node); + }, new Map()); + + // Regex used to extract entry-point name from an import statement referencing that entry-point. + // E.g., extract "portal" from "from '@angular/cdk/portal';". + const importRegex = new RegExp(`${packageName}/(.+)';`); + + // Update the deps for each node to point to the appropriate BuildNodes. + buildNodes.forEach(node => { + // Look for any imports that reference this same umbrella package and get the corresponding + // BuildNode for each by looking at the import statements with grep. + node.deps = spawnSync('grep', [ + '-Eroh', + '--include', '*.ts', + `from '@angular/${packageName}/.+';`, + `${packageDir}/${node.name}/` + ]) + .stdout + .toString() + .split('\n') + .filter(n => n) + .map(importStatement => importStatement.match(importRegex)![1]) + .filter(n => nodeLookup.has(n)) + .map(depName => nodeLookup.get(depName)!) || []; + }); + + // Concatenate the build order for each node into one global build order. + // Duplicates are automatically omitted by getBuildOrder. + return buildNodes.reduce((order: string[], node) => { + return [...order, ...getBuildOrder(node)]; + }, []); +} + +/** Gets the build order for a given node with DFS. */ +function getBuildOrder(node: BuildNode): string[] { + if (node.visited) { + return []; + } + + let buildOrder: string[] = []; + for (const dep of node.deps) { + buildOrder = [...buildOrder, ...getBuildOrder(dep)]; + } + + node.visited = true; + return [...buildOrder, node.name]; +} + +/** Gets the names of all subdirectories for a given path. */ +export function getSubdirectoryNames(dir: string): string[] { + return readdirSync(dir).filter(f => lstatSync(join(dir, f)).isDirectory()); +} + +/** A node in the build graph of a package's entry-points. */ +interface BuildNode { + name: string; + deps: BuildNode[]; + visited?: boolean; +} diff --git a/tools/package-tools/typings-reexport.ts b/tools/package-tools/typings-reexport.ts index 48d009ecf..d271f5ab2 100644 --- a/tools/package-tools/typings-reexport.ts +++ b/tools/package-tools/typings-reexport.ts @@ -3,8 +3,8 @@ import {buildConfig} from './build-config'; import {join} from 'path'; /** Create a typing file that links to the bundled definitions of NGC. */ -export function createTypingsReexportFile(outputDir: string, entryName: string) { - writeFileSync(join(outputDir, `${entryName}.d.ts`), - buildConfig.licenseBanner + '\nexport * from "./typings/index";' - ); +export function createTypingsReexportFile(outDir: string, from: string, fileName: string) { + writeFileSync(join(outDir, `${fileName}.d.ts`), + `${buildConfig.licenseBanner}\nexport * from '${from}';\n`, + 'utf-8'); } diff --git a/tools/package-tools/version-placeholders.ts b/tools/package-tools/version-placeholders.ts new file mode 100644 index 000000000..f8fa4db0d --- /dev/null +++ b/tools/package-tools/version-placeholders.ts @@ -0,0 +1,54 @@ +import {readFileSync, writeFileSync} from 'fs'; +import {platform} from 'os'; +import {buildConfig} from './build-config'; +import {spawnSync} from 'child_process'; + +/** Variable that is set to the string for version placeholders. */ +const versionPlaceholderText = '0.0.0-PLACEHOLDER'; + +/** RegExp that matches version placeholders inside of a file. */ +const versionPlaceholderRegex = new RegExp(versionPlaceholderText, 'g'); + +/** + * Walks through every file in a directory and replaces the version placeholders with the current + * version of Library. + */ +export function replaceVersionPlaceholders(packageDir: string) { + // Resolve files that contain version placeholders using Grep or Findstr since those are + // extremely fast and also have a very simple usage. + const files = findFilesWithPlaceholders(packageDir); + + // Walk through every file that contains version placeholders and replace those with the current + // version of the root package.json file. + files.forEach(filePath => { + let fileContent = readFileSync(filePath, 'utf-8'); + + fileContent = fileContent.replace(versionPlaceholderRegex, buildConfig.projectVersion); + + writeFileSync(filePath, fileContent); + }); +} + +/** Finds all files in the specified package dir where version placeholders are included. */ +function findFilesWithPlaceholders(packageDir: string): string[] { + const findCommand = buildPlaceholderFindCommand(packageDir); + return spawnSync(findCommand.binary, findCommand.args).stdout + .toString() + .split(/[\n\r]/) + .filter(String); +} + +/** Builds the command that will be executed to find all files containing version placeholders. */ +function buildPlaceholderFindCommand(packageDir: string) { + if (platform() === 'win32') { + return { + binary: 'findstr', + args: ['/msi', `/c:${versionPlaceholderText}`, `${packageDir}\\*`] + }; + } else { + return { + binary: 'grep', + args: ['-ril', versionPlaceholderText, packageDir] + }; + } +} diff --git a/tools/tslint-rules/missingRollupGlobalsRule.js b/tools/tslint-rules/missingRollupGlobalsRule.js new file mode 100644 index 000000000..b8df1727d --- /dev/null +++ b/tools/tslint-rules/missingRollupGlobalsRule.js @@ -0,0 +1,54 @@ +const path = require('path'); +const Lint = require('tslint'); +const minimatch = require('minimatch'); + +// Since the packaging is based on TypeScript and is only compiled at run-time using ts-node, the +// custom TSLint rule is not able to read the map of rollup globals. Because the custom rules +// for TSLint are written in JavaScript we also need to use ts-node here to read the globals. +require('ts-node').register({ + project: path.join(__dirname, '../gulp/tsconfig.json') +}); + +/** + * Rule that enforces that the specified external packages have been included in our Rollup config. + * Usage: [true, './path/to/rollup/config.json'] + */ +class Rule extends Lint.Rules.AbstractRule { + apply(file) { + return this.applyWithWalker(new Walker(file, this.getOptions())); + } +} + +class Walker extends Lint.RuleWalker { + constructor(file, options) { + super(...arguments); + + if (!options.ruleArguments.length) { + throw Error('missing-rollup-globals: The Rollup config path has to be specified.'); + } + + const [configPath, ...fileGlobs] = options.ruleArguments; + + // Relative path for the current TypeScript source file. + const relativeFilePath = path.relative(process.cwd(), file.fileName); + + this._configPath = path.resolve(process.cwd(), configPath); + this._config = require(this._configPath).rollupGlobals; + this._enabled = fileGlobs.some(p => minimatch(relativeFilePath, p)); + } + + visitImportDeclaration(node) { + // Parse out the module name. The first and last characters are the quote marks. + const module = node.moduleSpecifier.getText().slice(1, -1); + const isExternal = !module.startsWith('.') && !module.startsWith('/'); + + // Check whether the module is external and whether it's in our config. + if (this._enabled && isExternal && !this._config[module]) { + this.addFailureAtNode(node, `Module "${module}" is missing from file ${this._configPath}.`); + } + + super.visitImportDeclaration(node); + } +} + +exports.Rule = Rule; diff --git a/tools/tslint-rules/noExposedTodoRule.js b/tools/tslint-rules/noExposedTodoRule.js index c92eb9a4a..f7f34ac0d 100644 --- a/tools/tslint-rules/noExposedTodoRule.js +++ b/tools/tslint-rules/noExposedTodoRule.js @@ -28,6 +28,8 @@ class NoExposedTodoWalker extends Lint.RuleWalker { this.addFailureAt(commentRange.pos, commentRange.end - commentRange.pos, ERROR_MESSAGE); } }); + + super.visitSourceFile(sourceFile); } } diff --git a/tools/tslint-rules/noRxjsPatchImportsRule.js b/tools/tslint-rules/noRxjsPatchImportsRule.js index 3fc9c88bf..119058331 100644 --- a/tools/tslint-rules/noRxjsPatchImportsRule.js +++ b/tools/tslint-rules/noRxjsPatchImportsRule.js @@ -1,4 +1,7 @@ const Lint = require('tslint'); +const minimatch = require('minimatch'); +const path = require('path'); + const ERROR_MESSAGE = 'Uses of RxJS patch imports are forbidden.'; /** @@ -15,11 +18,14 @@ class Walker extends Lint.RuleWalker { constructor(file, options) { super(...arguments); - // Whitelist with regular expressions to use when determining which files to lint. - const whitelist = options.ruleArguments; + // Globs that are used to determine which files to lint. + const fileGlobs = options.ruleArguments || []; + + // Relative path for the current TypeScript source file. + const relativeFilePath = path.relative(process.cwd(), file.fileName); // Whether the file should be checked at all. - this._enabled = !whitelist.length || whitelist.some(p => new RegExp(p).test(file.fileName)); + this._enabled = fileGlobs.some(p => minimatch(relativeFilePath, p)); } visitImportDeclaration(node) { diff --git a/tools/tslint-rules/requireLicenseBannerRule.js b/tools/tslint-rules/requireLicenseBannerRule.js index 4f710e889..668dbb09f 100644 --- a/tools/tslint-rules/requireLicenseBannerRule.js +++ b/tools/tslint-rules/requireLicenseBannerRule.js @@ -1,5 +1,6 @@ const Lint = require('tslint'); const path = require('path'); +const minimatch = require('minimatch'); const buildConfig = require('../../build-config'); /** Paths to the directories that are public packages and should be validated. */ @@ -31,16 +32,21 @@ class Rule extends Lint.Rules.AbstractRule { class RequireLicenseBannerWalker extends Lint.RuleWalker { - visitSourceFile(sourceFile) { - const filePath = path.join(buildConfig.projectDir, sourceFile.fileName); + constructor(file, options) { + super(...arguments); - // Do not check TypeScript source files that are not inside of a public package. - if (!packageDirs.some(packageDir => filePath.includes(packageDir))) { - return; - } + // Globs that are used to determine which files to lint. + const fileGlobs = options.ruleArguments; + + // Relative path for the current TypeScript source file. + const relativeFilePath = path.relative(process.cwd(), file.fileName); - // Do not check source files inside of public packages that are spec or definition files. - if (filePath.endsWith('.spec.ts') || filePath.endsWith('.d.ts')) { + // Whether the file should be checked at all. + this._enabled = fileGlobs.some(p => minimatch(relativeFilePath, p)); + } + + visitSourceFile(sourceFile) { + if (!this._enabled) { return; } @@ -50,6 +56,8 @@ class RequireLicenseBannerWalker extends Lint.RuleWalker { if (licenseCommentPos !== 0) { return this.addFailureAt(0, 0, ERROR_MESSAGE, tslintFix); } + + super.visitSourceFile(sourceFile); } }