From f6052a915d7225c2a5bfe7d4af30d1f77d0c71c9 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Sun, 27 Sep 2020 13:37:43 +0300 Subject: [PATCH] build(docs-infra): simplify update workflow for CLI-based docs examples boilerplate (#38992) When updating the boilerplate for CLI-based docs examples, one needed to install dependencies inside the `aio/tools/examples/shared/boilerplate/cli/` directory, which resulted in a `node_modules/` directory and a `yarn.lock` file. These were not supposed to be part of the boilerplate, so they had to be manually removed after the boilerplate was updated. This commit simplifies the workflow by allowing boilerplate files to be ignored (both by git and the `example-boilerplate.js` script) via a `.gitignore` file. This way, it is no longer necessary to manually remove the unneeded directories/files. PR Close #38992 --- aio/tools/examples/UPDATING.md | 5 +- aio/tools/examples/example-boilerplate.js | 38 +++-- .../examples/example-boilerplate.spec.js | 143 ++++++++++++------ .../examples/shared/boilerplate/.gitignore | 3 +- 4 files changed, 120 insertions(+), 69 deletions(-) diff --git a/aio/tools/examples/UPDATING.md b/aio/tools/examples/UPDATING.md index 2298d9f678..773d59f230 100644 --- a/aio/tools/examples/UPDATING.md +++ b/aio/tools/examples/UPDATING.md @@ -32,14 +32,11 @@ Any necessary changes to boilerplate files will be done automatically through mi # Migrate project to new versions. yarn ng update @angular/cli --migrate-only --from= yarn ng update @angular/core --migrate-only --from= - - # Remove `node_modules/` and `yarn.lock`. - rm -rf node_modules yarn.lock ``` > NOTE: > In order for `ng update` to work, there must be a `node_modules/` directory with installed dependencies inside the [shared/boilerplate/cli/](./shared/boilerplate/cli) directory. - > This `node_modules/` directory is only needed during the update operation and must be subsequently removed to avoid its being copied into all CLI-based examples. + > This `node_modules/` directory is only needed during the update operation and is otherwise ignored (both by git and by the [example-boilerplate.js](./example-boilerplate.js) script) by means of the [shared/boilerplate/.gitignore](./shared/boilerplate/.gitignore) file. - The previous command made any necessary changes to boilerplate files inside the `cli/` folder, but the same changes need to be applied to the other CLI-based boilerplate folders. Inspect the changes in `cli/` and manually apply the necessary ones to other CLI-based boilerplate folders. diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index bc1c939656..a77ac0dd88 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -1,5 +1,6 @@ const fs = require('fs-extra'); const glob = require('glob'); +const ignore = require('ignore'); const path = require('canonical-path'); const shelljs = require('shelljs'); const yargs = require('yargs'); @@ -23,6 +24,8 @@ class ExampleBoilerPlate { // Get all the examples folders, indicated by those that contain a `example-config.json` file const exampleFolders = this.getFoldersContaining(EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, 'node_modules'); + const gitignore = ignore().add(fs.readFileSync(path.resolve(BOILERPLATE_BASE_PATH, '.gitignore'), 'utf8')); + const isPathIgnored = absolutePath => gitignore.ignores(path.relative(BOILERPLATE_BASE_PATH, absolutePath)); if (!fs.existsSync(SHARED_NODE_MODULES_PATH)) { throw new Error( @@ -48,22 +51,22 @@ class ExampleBoilerPlate { // boilerplate files first. // (Some of these files might be later overwritten by type-specific files.) if (boilerPlateType !== 'cli' && boilerPlateType !== 'systemjs') { - this.copyDirectoryContents(BOILERPLATE_CLI_PATH, exampleFolder); + this.copyDirectoryContents(BOILERPLATE_CLI_PATH, exampleFolder, isPathIgnored); } // Copy the type-specific boilerplate files. - this.copyDirectoryContents(boilerPlateBasePath, exampleFolder); + this.copyDirectoryContents(boilerPlateBasePath, exampleFolder, isPathIgnored); // Copy the common boilerplate files (unless explicitly not used). if (exampleConfig.useCommonBoilerplate !== false) { - this.copyDirectoryContents(BOILERPLATE_COMMON_PATH, exampleFolder); + this.copyDirectoryContents(BOILERPLATE_COMMON_PATH, exampleFolder, isPathIgnored); } // Copy ViewEngine (pre-Ivy) specific files if (viewengine) { const veBoilerPlateType = boilerPlateType === 'systemjs' ? 'systemjs' : 'cli'; const veBoilerPlateBasePath = path.resolve(BOILERPLATE_VIEWENGINE_PATH, veBoilerPlateType); - this.copyDirectoryContents(veBoilerPlateBasePath, exampleFolder); + this.copyDirectoryContents(veBoilerPlateBasePath, exampleFolder, isPathIgnored); } }); } @@ -89,25 +92,30 @@ class ExampleBoilerPlate { loadJsonFile(filePath) { return fs.readJsonSync(filePath, {throws: false}) || {}; } - copyDirectoryContents(srcDir, dstDir) { + copyDirectoryContents(srcDir, dstDir, isPathIgnored) { shelljs.ls('-Al', srcDir).forEach(stat => { const srcPath = path.resolve(srcDir, stat.name); const dstPath = path.resolve(dstDir, stat.name); + if (isPathIgnored(srcPath)) { + // `srcPath` is ignored (e.g. by a `.gitignore` file): Ignore it. + return; + } + if (stat.isDirectory()) { // `srcPath` is a directory: Recursively copy it to `dstDir`. shelljs.mkdir('-p', dstPath); - return this.copyDirectoryContents(srcPath, dstPath); - } else { - // `srcPath` is a file: Copy it to `dstDir`. - // (Also make the file non-writable to avoid accidental editing of boilerplate files). - if (shelljs.test('-f', dstPath)) { - // If the file already exists, ensure it is writable (so it can be overwritten). - shelljs.chmod(666, dstPath); - } - shelljs.cp(srcPath, dstDir); - shelljs.chmod(444, dstPath); + return this.copyDirectoryContents(srcPath, dstPath, isPathIgnored); } + + // `srcPath` is a file: Copy it to `dstDir`. + // (Also make the file non-writable to avoid accidental editing of boilerplate files). + if (shelljs.test('-f', dstPath)) { + // If the file already exists, ensure it is writable (so it can be overwritten). + shelljs.chmod(666, dstPath); + } + shelljs.cp(srcPath, dstDir); + shelljs.chmod(444, dstPath); }); } } diff --git a/aio/tools/examples/example-boilerplate.spec.js b/aio/tools/examples/example-boilerplate.spec.js index cff911a1c3..f716f5c434 100644 --- a/aio/tools/examples/example-boilerplate.spec.js +++ b/aio/tools/examples/example-boilerplate.spec.js @@ -56,10 +56,10 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/systemjs`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/systemjs`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], + [`${boilerplateDir}/systemjs`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/systemjs`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], ]); }); @@ -71,10 +71,10 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/cli`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/cli`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], + [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], ]); }); @@ -86,10 +86,10 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/cli`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/cli`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], + [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], ]); }); @@ -101,12 +101,12 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/cli`, 'a/b'], - [`${boilerplateDir}/i18n`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/cli`, 'c/d'], - [`${boilerplateDir}/i18n`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], + [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/i18n`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/i18n`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], ]); }); @@ -118,12 +118,12 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/cli`, 'a/b'], - [`${boilerplateDir}/universal`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/cli`, 'c/d'], - [`${boilerplateDir}/universal`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], + [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/universal`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/universal`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], ]); }); @@ -148,12 +148,12 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/systemjs`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/viewengine/systemjs`, 'a/b'], - [`${boilerplateDir}/systemjs`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], - [`${boilerplateDir}/viewengine/systemjs`, 'c/d'], + [`${boilerplateDir}/systemjs`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/viewengine/systemjs`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/systemjs`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/viewengine/systemjs`, 'c/d', jasmine.any(Function)], ]); }); @@ -165,12 +165,12 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/cli`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/viewengine/cli`, 'a/b'], - [`${boilerplateDir}/cli`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], - [`${boilerplateDir}/viewengine/cli`, 'c/d'], + [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/viewengine/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/viewengine/cli`, 'c/d', jasmine.any(Function)], ]); }); @@ -182,14 +182,14 @@ describe('example-boilerplate tool', () => { expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(8); expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ - [`${boilerplateDir}/cli`, 'a/b'], - [`${boilerplateDir}/elements`, 'a/b'], - [`${boilerplateDir}/common`, 'a/b'], - [`${boilerplateDir}/viewengine/cli`, 'a/b'], - [`${boilerplateDir}/cli`, 'c/d'], - [`${boilerplateDir}/elements`, 'c/d'], - [`${boilerplateDir}/common`, 'c/d'], - [`${boilerplateDir}/viewengine/cli`, 'c/d'], + [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/elements`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/viewengine/cli`, 'a/b', jasmine.any(Function)], + [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/elements`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)], + [`${boilerplateDir}/viewengine/cli`, 'c/d', jasmine.any(Function)], ]); }); }); @@ -214,10 +214,12 @@ describe('example-boilerplate tool', () => { describe('copyDirectoryContents', () => { const spyFnFor = fnName => (...args) => { callLog.push(`${fnName}(${args.join(', ')})`); }; + let isPathIgnoredSpy; let callLog; beforeEach(() => { callLog = []; + isPathIgnoredSpy = jasmine.createSpy('isPathIgnored').and.returnValue(false); spyOn(shelljs, 'chmod').and.callFake(spyFnFor('chmod')); spyOn(shelljs, 'cp').and.callFake(spyFnFor('cp')); spyOn(shelljs, 'mkdir').and.callFake(spyFnFor('mkdir')); @@ -226,17 +228,17 @@ describe('example-boilerplate tool', () => { it('should list all contents of a directory', () => { const lsSpy = spyOn(shelljs, 'ls').and.returnValue([]); - exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir', isPathIgnoredSpy); expect(lsSpy).toHaveBeenCalledWith('-Al', 'source/dir'); }); - it('should use copy files and make them read-only', () => { + it('should copy files and make them read-only', () => { spyOn(shelljs, 'ls').and.returnValue([ {name: 'file-1.txt', isDirectory: () => false}, {name: 'file-2.txt', isDirectory: () => false}, ]); - exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir', isPathIgnoredSpy); expect(callLog).toEqual([ `test(-f, ${path.resolve('destination/dir/file-1.txt')})`, @@ -249,6 +251,22 @@ describe('example-boilerplate tool', () => { ]); }); + it('should skip files that are ignored', () => { + spyOn(shelljs, 'ls').and.returnValue([ + {name: 'file-1.txt', isDirectory: () => false}, + {name: 'file-2.txt', isDirectory: () => false}, + ]); + isPathIgnoredSpy.and.callFake(path => path.endsWith('file-1.txt')); + + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir', isPathIgnoredSpy); + + expect(callLog).toEqual([ + `test(-f, ${path.resolve('destination/dir/file-2.txt')})`, + `cp(${path.resolve('source/dir/file-2.txt')}, destination/dir)`, + `chmod(444, ${path.resolve('destination/dir/file-2.txt')})`, + ]); + }); + it('should make existing files in destination writable before overwriting', () => { spyOn(shelljs, 'ls').and.returnValue([ {name: 'new-file.txt', isDirectory: () => false}, @@ -256,7 +274,7 @@ describe('example-boilerplate tool', () => { ]); shelljs.test.and.callFake((_, filePath) => filePath.endsWith('existing-file.txt')); - exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir', isPathIgnoredSpy); expect(callLog).toEqual([ `cp(${path.resolve('source/dir/new-file.txt')}, destination/dir)`, @@ -283,7 +301,7 @@ describe('example-boilerplate tool', () => { {name: 'file-4.txt', isDirectory: () => false}, ]); - exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir', isPathIgnoredSpy); expect(callLog).toEqual([ // Copy `file-1.txt`. @@ -317,6 +335,33 @@ describe('example-boilerplate tool', () => { `chmod(444, ${path.resolve('destination/dir/file-2.txt')})`, ]); }); + + it('should skip ignored directories', () => { + spyOn(shelljs, 'ls') + .withArgs('-Al', 'source/dir').and.returnValue([ + {name: 'file-1.txt', isDirectory: () => false}, + {name: 'sub-dir-1', isDirectory: () => true}, + ]) + .withArgs('-Al', path.resolve('source/dir/sub-dir-1')).and.returnValue([ + {name: 'file-2.txt', isDirectory: () => false}, + {name: 'sub-dir-2', isDirectory: () => true}, + ]) + .withArgs('-Al', path.resolve('source/dir/sub-dir-1/sub-dir-2')).and.returnValue([ + {name: 'file-3.txt', isDirectory: () => false}, + ]); + isPathIgnoredSpy.and.callFake(path => path.endsWith('sub-dir-1')); + + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir', isPathIgnoredSpy); + + expect(callLog).toEqual([ + // Copy `file-1.txt`. + `test(-f, ${path.resolve('destination/dir/file-1.txt')})`, + `cp(${path.resolve('source/dir/file-1.txt')}, destination/dir)`, + `chmod(444, ${path.resolve('destination/dir/file-1.txt')})`, + + // Skip `sub-dir-1` and all its contents. + ]); + }); }); describe('loadJsonFile', () => { diff --git a/aio/tools/examples/shared/boilerplate/.gitignore b/aio/tools/examples/shared/boilerplate/.gitignore index ceed8f2aed..908cc87463 100644 --- a/aio/tools/examples/shared/boilerplate/.gitignore +++ b/aio/tools/examples/shared/boilerplate/.gitignore @@ -1,2 +1,3 @@ +**/node_modules/ +**/package-lock.json **/yarn.lock -**/package-lock.json \ No newline at end of file