From ecff8e6c93b98ed08dd7d5212042c0bb4a85bed1 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 27 Jul 2017 08:29:17 +0100 Subject: [PATCH] build(aio): refactor and test the example-boilerplate tool --- aio/package.json | 5 +- aio/tools/examples/add-example-boilerplate.js | 148 ------------------ aio/tools/examples/example-boilerplate.js | 103 ++++++++++++ .../examples/example-boilerplate.spec.js | 115 ++++++++++++++ aio/tools/examples/test.js | 17 ++ 5 files changed, 238 insertions(+), 150 deletions(-) delete mode 100644 aio/tools/examples/add-example-boilerplate.js create mode 100644 aio/tools/examples/example-boilerplate.js create mode 100644 aio/tools/examples/example-boilerplate.spec.js create mode 100644 aio/tools/examples/test.js diff --git a/aio/package.json b/aio/package.json index 7aaeb6657d..b81a42b28f 100644 --- a/aio/package.json +++ b/aio/package.json @@ -33,8 +33,9 @@ "docs-test": "node tools/transforms/test.js", "serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"", "~~update-webdriver": "webdriver-manager update --standalone false --gecko false", - "boilerplate:add": "node ./tools/examples/add-example-boilerplate add", - "boilerplate:remove": "node ./tools/examples/add-example-boilerplate remove", + "boilerplate:add": "node ./tools/examples/example-boilerplate add", + "boilerplate:remove": "node ./tools/examples/example-boilerplate remove", + "boilerplate:test": "node tools/examples/test.js", "generate-plunkers": "node ./tools/plunker-builder/generatePlunkers", "generate-zips": "node ./tools/example-zipper/generateZips", "sw-manifest": "ngu-sw-manifest --dist dist --in ngsw-manifest.json --out dist/ngsw-manifest.json", diff --git a/aio/tools/examples/add-example-boilerplate.js b/aio/tools/examples/add-example-boilerplate.js deleted file mode 100644 index 4a74ddc6e0..0000000000 --- a/aio/tools/examples/add-example-boilerplate.js +++ /dev/null @@ -1,148 +0,0 @@ -const fs = fsExtra = require('fs-extra'); -const globby = require('globby'); -const path = require('path'); -const Q = require("q"); -const shelljs = require('shelljs'); - -const EXAMPLES_PATH = path.join(__dirname, '/../../content/examples'); -const SHARED_PATH = path.join(__dirname, '/shared'); -const BOILERPLATE_PATH = path.join(SHARED_PATH, 'boilerplate'); -const EXAMPLES_TESTING_PATH = path.join(EXAMPLES_PATH, 'testing'); - -const files = { - exampleBoilerplate: [ - 'src/styles.css', - 'src/systemjs-angular-loader.js', - 'src/systemjs.config.js', - 'src/tsconfig.json', - 'bs-config.json', - 'bs-config.e2e.json', - 'package.json', - 'tslint.json' - ], - exampleUnitTestingBoilerplate: [ - 'src/browser-test-shim.js', - 'karma-test-shim.js', - 'karma.conf.js' - ], - exampleConfigFilename: 'example-config.json' -}; - -// requires admin access because it adds symlinks -function add() { - const realPath = path.join(SHARED_PATH, '/node_modules'); - const nodeModulesPaths = getNodeModulesPaths(EXAMPLES_PATH); - - // we install the examples modules first - installNodeModules(); - - nodeModulesPaths.map((linkPath) => { - fs.ensureSymlinkSync(realPath, linkPath); - }); - - return copyExampleBoilerplate(); -} - -function copyExampleBoilerplate() { - console.log('Copying example boilerplate files'); - const examplePaths = getExamplePaths(EXAMPLES_PATH); - // Make boilerplate files read-only to avoid that they be edited by mistake. - const destFileMode = '444'; - let foo = copyFiles(files.exampleBoilerplate, BOILERPLATE_PATH, examplePaths, destFileMode) - // copy the unit test boilerplate - .then(() => { - const unittestPaths = getUnitTestingPaths(EXAMPLES_PATH); - return copyFiles(files.exampleUnitTestingBoilerplate, EXAMPLES_TESTING_PATH, unittestPaths, destFileMode); - }) - .catch((err) => { - console.error(err); - throw err; - }); -} - -// Copies fileNames into destPaths, setting the mode of the -// files at the destination as optional_destFileMode if given. -// returns a promise -function copyFiles(fileNames, originPath, destPaths, optional_destFileMode) { - const copy = Q.denodeify(fsExtra.copy); - const chmod = Q.denodeify(fsExtra.chmod); - let copyPromises = []; - destPaths.map((destPath) => { - fileNames.forEach((fileName) => { - const originName = path.join(originPath, fileName); - const destName = path.join(destPath, fileName); - let p = copy(originName, destName, { clobber: true}); - if(optional_destFileMode !== undefined) { - p = p.then(() => { - return chmod(destName, optional_destFileMode); - }); - } - copyPromises.push(p); - }); - }); - - return Q.all(copyPromises); -} - -function getExamplePaths(basePath, includeBase) { - // includeBase defaults to false - return getPaths(basePath, files.exampleConfigFilename, includeBase); -} - -function getFilenames(basePath, filename, includeBase) { - let includePatterns = [path.join(basePath, "**/" + filename)]; - if (!includeBase) { - // ignore (skip) the top level version. - includePatterns.push("!" + path.join(basePath, "/" + filename)); - } - // ignore (skip) the files in BOILERPLATE_PATH. - includePatterns.push("!" + path.join(BOILERPLATE_PATH, "/" + filename)); - const nmPattern = path.join(basePath, "**/node_modules/**"); - return globby.sync(includePatterns, {ignore: [nmPattern]}); -} - -function getNodeModulesPaths(basePath) { - return getExamplePaths(basePath).map((examplePath) => { - return path.join(examplePath, "/node_modules"); - }); -} - -function getPaths(basePath, filename, includeBase) { - const filenames = getFilenames(basePath, filename, includeBase); - return filenames.map((fileName) => { - return path.dirname(fileName); - }); -} - -function getUnitTestingPaths(basePath) { - const examples = getPaths(basePath, files.exampleConfigFilename, true); - return examples.filter((example) => { - const exampleConfig = fs.readJsonSync(`${example}/${files.exampleConfigFilename}`, {throws: false}); - return exampleConfig && !!exampleConfig.unittesting; - }); -} - -function installNodeModules() { - shelljs.exec('yarn', {cwd: SHARED_PATH}); -} - -function remove() { - shelljs.exec('git clean -xdf', {cwd: EXAMPLES_PATH}); -} - -module.exports = { - add: add, - remove: remove -}; - -// being executed from shell script -switch (process.argv[2]) { - case 'add': - add(); - break; - case 'remove': - remove(); - break; - default: - console.error(`There is no function with the name: ${process.argv}`); -} diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js new file mode 100644 index 0000000000..08ab293da5 --- /dev/null +++ b/aio/tools/examples/example-boilerplate.js @@ -0,0 +1,103 @@ +const fs = require('fs-extra'); +const glob = require('glob'); +const path = require('canonical-path'); +const shelljs = require('shelljs'); +const yargs = require('yargs'); + +const SHARED_PATH = path.resolve(__dirname, 'shared'); +const SHARED_NODE_MODULES_PATH = path.resolve(SHARED_PATH, 'node_modules'); +const BOILERPLATE_BASE_PATH = path.resolve(SHARED_PATH, 'boilerplate'); +const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples'); +const TESTING_BASE_PATH = path.resolve(EXAMPLES_BASE_PATH, 'testing'); + +const BOILERPLATE_SRC_PATHS = [ + 'src/styles.css', + 'src/systemjs-angular-loader.js', + 'src/systemjs.config.js', + 'src/tsconfig.json', + 'bs-config.json', + 'bs-config.e2e.json', + 'package.json', + 'tslint.json' +]; + +const BOILERPLATE_TEST_PATHS = [ + 'src/browser-test-shim.js', + 'karma-test-shim.js', + 'karma.conf.js' +]; + +const EXAMPLE_CONFIG_FILENAME = 'example-config.json'; + +class ExampleBoilerPlate { + /** + * Add boilerplate files to all the examples + * + */ + add() { + // first install the shared node_modules + this.installNodeModules(SHARED_PATH); + + // 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'); + exampleFolders.forEach(exampleFolder => { + + // Link the node modules - requires admin access (on Windows) because it adds symlinks + const destinationNodeModules = path.resolve(exampleFolder, 'node_modules'); + fs.ensureSymlinkSync(SHARED_NODE_MODULES_PATH, destinationNodeModules); + + // Copy the boilerplate source files + BOILERPLATE_SRC_PATHS.forEach(filePath => this.copyFile(BOILERPLATE_BASE_PATH, exampleFolder, filePath)); + + // Copy the boilerplate test files (if the example is configured to do unit testing) + const exampleConfig = this.loadJsonFile(path.resolve(exampleFolder, EXAMPLE_CONFIG_FILENAME)); + if (exampleConfig.unittesting) { + BOILERPLATE_TEST_PATHS.forEach(filePath => this.copyFile(TESTING_BASE_PATH, exampleFolder, filePath)); + } + }); + } + + /** + * Remove all the boilerplate files from all the examples + */ + remove() { + shelljs.exec('git clean -xdfq', {cwd: EXAMPLES_BASE_PATH}); + } + + main() { + yargs + .usage('$0 [args]') + .command('add', 'add the boilerplate to each example', argv => this.add(argv.local)) + .command('remove', 'remove the boilerplate from each example', () => this.remove()) + .demandCommand(1, 'Please supply a command from the list above') + .argv; + } + + installNodeModules(basePath) { + shelljs.exec('yarn', {cwd: basePath}); + } + + getFoldersContaining(basePath, filename, ignore) { + const pattern = path.resolve(basePath, '**', filename); + const ignorePattern = path.resolve(basePath, '**', ignore, '**'); + return glob.sync(pattern, { ignore: [ignorePattern] }).map(file => path.dirname(file)); + } + + copyFile(sourceFolder, destinationFolder, filePath) { + const sourcePath = path.resolve(sourceFolder, filePath); + const destinationPath = path.resolve(destinationFolder, filePath); + fs.copySync(sourcePath, destinationPath, { overwrite: true }); + fs.chmodSync(destinationPath, 444); + } + + loadJsonFile(filePath) { + return fs.readJsonSync(filePath, {throws: false}) || {}; + } +} + +module.exports = new ExampleBoilerPlate(); + +// If this file was run directly then run the main function, +if (require.main === module) { + module.exports.main(); +} \ No newline at end of file diff --git a/aio/tools/examples/example-boilerplate.spec.js b/aio/tools/examples/example-boilerplate.spec.js new file mode 100644 index 0000000000..dc44a71cf4 --- /dev/null +++ b/aio/tools/examples/example-boilerplate.spec.js @@ -0,0 +1,115 @@ +const exampleBoilerPlate = require('./example-boilerplate'); +const shelljs = require('shelljs'); +const fs = require('fs-extra'); +const glob = require('glob'); +const path = require('canonical-path'); + +describe('example-boilerplate tool', () => { + describe('add', () => { + const numberOfBoilerPlateFiles = 8; + const numberOfBoilerPlateTestFiles = 3; + const exampleFolders = ['a/b', 'c/d']; + + beforeEach(() => { + spyOn(exampleBoilerPlate, 'installNodeModules'); + spyOn(exampleBoilerPlate, 'getFoldersContaining').and.returnValue(exampleFolders); + spyOn(fs, 'ensureSymlinkSync'); + spyOn(exampleBoilerPlate, 'copyFile'); + spyOn(exampleBoilerPlate, 'loadJsonFile').and.returnValue({}); + }); + + it('should install the node modules', () => { + exampleBoilerPlate.add(); + expect(exampleBoilerPlate.installNodeModules).toHaveBeenCalledWith(path.resolve(__dirname, 'shared')); + }); + + it('should process all the example folders', () => { + exampleBoilerPlate.add(); + expect(exampleBoilerPlate.getFoldersContaining).toHaveBeenCalledWith(path.resolve(__dirname, '../../content/examples'), 'example-config.json', 'node_modules'); + }); + + it('should symlink the node_modules', () => { + exampleBoilerPlate.add(); + expect(fs.ensureSymlinkSync).toHaveBeenCalledTimes(exampleFolders.length); + expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/node_modules'), path.resolve('a/b/node_modules')); + expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/node_modules'), path.resolve('c/d/node_modules')); + }); + + it('should copy all the source boilerplate files', () => { + exampleBoilerPlate.add(); + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes(numberOfBoilerPlateFiles * exampleFolders.length); + // for example + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/boilerplate'), 'a/b', 'package.json'); + }); + + it('should try to load the example config file', () => { + exampleBoilerPlate.add(); + expect(exampleBoilerPlate.loadJsonFile).toHaveBeenCalledTimes(exampleFolders.length); + expect(exampleBoilerPlate.loadJsonFile).toHaveBeenCalledWith(path.resolve('a/b/example-config.json')); + expect(exampleBoilerPlate.loadJsonFile).toHaveBeenCalledWith(path.resolve('c/d/example-config.json')); + }); + + it('should copy all the test boilerplate files if unit testing is configured', () => { + // configure unit testing for example a/b and not c/d + exampleBoilerPlate.loadJsonFile.and.callFake(filePath => filePath.indexOf('a/b') !== -1 ? { unittesting: true } : {}); + exampleBoilerPlate.add(); + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes((numberOfBoilerPlateFiles * 2) + numberOfBoilerPlateTestFiles); + // for example + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(path.resolve(__dirname, '../../content/examples/testing'), 'a/b', 'karma.conf.js'); + expect(exampleBoilerPlate.copyFile).not.toHaveBeenCalledWith(path.resolve(__dirname, '../../content/examples/testing'), 'c/d', 'karma.conf.js'); + }); + }); + + describe('remove', () => { + it('should run `git clean`', () => { + spyOn(shelljs, 'exec'); + exampleBoilerPlate.remove(); + expect(shelljs.exec).toHaveBeenCalledWith('git clean -xdfq', {cwd: path.resolve(__dirname, '../../content/examples') }); + }); + }); + + describe('installNodeModules', () => { + it('should run `yarn` in the base path', () => { + spyOn(shelljs, 'exec'); + exampleBoilerPlate.installNodeModules('some/base/path'); + expect(shelljs.exec).toHaveBeenCalledWith('yarn', { cwd: 'some/base/path' }); + }); + }); + + describe('getFoldersContaining', () => { + it('should use glob.sync', () => { + spyOn(glob, 'sync').and.returnValue(['a/b/config.json', 'c/d/config.json']); + const result = exampleBoilerPlate.getFoldersContaining('base/path', 'config.json', 'node_modules'); + expect(glob.sync).toHaveBeenCalledWith(path.resolve('base/path/**/config.json'), { ignore: [path.resolve('base/path/**/node_modules/**')] }); + expect(result).toEqual(['a/b', 'c/d']); + }); + }); + + describe('copyFile', () => { + it('should use copySync and chmodSync', () => { + spyOn(fs, 'copySync'); + spyOn(fs, 'chmodSync'); + exampleBoilerPlate.copyFile('source/folder', 'destination/folder', 'some/file/path'); + expect(fs.copySync).toHaveBeenCalledWith( + path.resolve('source/folder/some/file/path'), + path.resolve('destination/folder/some/file/path'), + { overwrite: true }); + expect(fs.chmodSync).toHaveBeenCalledWith(path.resolve('destination/folder/some/file/path'), 444); + }); + }); + + describe('loadJsonFile', () => { + it('should use fs.readJsonSync', () => { + spyOn(fs, 'readJsonSync').and.returnValue({ some: 'value' }); + const result = exampleBoilerPlate.loadJsonFile('some/file'); + expect(fs.readJsonSync).toHaveBeenCalledWith('some/file', {throws: false}); + expect(result).toEqual({ some: 'value' }); + }); + + it('should return an empty object if readJsonSync fails', () => { + spyOn(fs, 'readJsonSync').and.returnValue(null); + const result = exampleBoilerPlate.loadJsonFile('some/file'); + expect(result).toEqual({}); + }); + }); +}); \ No newline at end of file diff --git a/aio/tools/examples/test.js b/aio/tools/examples/test.js new file mode 100644 index 0000000000..4620f9204f --- /dev/null +++ b/aio/tools/examples/test.js @@ -0,0 +1,17 @@ +/* + * Use this script to run the tests for the doc generation + * We cannot use the Jasmine CLI directly because it doesn't seem to + * understand the glob and only runs one spec file. + * + * Equally we cannot use a jasmine.json config file because it doesn't + * allow us to set the projectBaseDir, which means that you have to run + * jasmine CLI from this directory. + * + * Using a file like this gives us full control and keeps the package.json + * file clean and simple. + */ + +const Jasmine = require('jasmine'); +const jasmine = new Jasmine({ projectBaseDir: __dirname }); +jasmine.loadConfig({ spec_files: ['*.spec.js'] }); +jasmine.execute();