From 5a733629b531083fd35563ee5bc286a8954da5fc Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Wed, 22 Jul 2020 14:40:03 +0300 Subject: [PATCH] build(docs-infra): remove boilerplate file listings in `example-boilerplate.js` (#38173) To avoid unnecessary code duplication in docs examples, we have some boilerplate files for various example types (in `aio/tools/examples/shared/boilerplate/`). These files are copied to each example project in `aio/content/examples/` (according to the example's type, as specified in its `example-config.json` file). Previously, the `example-boilerplate.js`, which is responsible for copying the boilerplate files, had lists for files to be copied for each project type and only copied the listed files from the boilerplate directory to the example directory. This approach had some drawbacks: - Files need to be updated in two separate locations: in the boilerplate directory that includes the files and the file list in `example-boilerplate.js`. - It is easy to add a file in the boilerplate directory but forget to add it in `example-boilerplate.js` and not realize that it is not being included in the example project (including the generated StackBlitz project and ZIP archive). This commit changes the approach by removing the boilerplate file listings from `example-boilerplate.js` and copying all files from a boilerplate directory to example directories. This addresses the above drawbacks and simplifies the `example-boilerplate.js` script. I have verified that the resulting code example doc regions as well as the generated StackBlitz projects and ZIP archives are identical to the ones generated before this commit. PR Close #38173 --- aio/tools/examples/example-boilerplate.js | 120 +++----- .../examples/example-boilerplate.spec.js | 284 +++++++++++++----- 2 files changed, 247 insertions(+), 157 deletions(-) diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index b0a4723873..bc1c939656 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -6,65 +6,13 @@ 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 BOILERPLATE_COMMON_BASE_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'common'); +const BOILERPLATE_CLI_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'cli'); +const BOILERPLATE_COMMON_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'common'); +const BOILERPLATE_VIEWENGINE_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'viewengine'); + const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples'); - -const BOILERPLATE_PATHS = { - cli: [ - 'src/environments/environment.prod.ts', 'src/environments/environment.ts', - 'src/assets/.gitkeep', 'browserslist', 'src/favicon.ico', 'karma.conf.js', - 'src/polyfills.ts', 'src/test.ts', 'tsconfig.app.json', 'tsconfig.spec.json', - 'tslint.json', 'e2e/src/app.po.ts', 'e2e/protractor-puppeteer.conf.js', - 'e2e/protractor.conf.js', 'e2e/tsconfig.json', '.editorconfig', '.gitignore', 'angular.json', - 'package.json', 'tsconfig.json', 'tslint.json' - ], - systemjs: [ - 'src/systemjs-angular-loader.js', 'src/systemjs.config.js', 'src/tsconfig.json', - 'bs-config.json', 'bs-config.e2e.json', 'package.json', 'tslint.json' - ], - common: ['src/styles.css'] -}; - -// All paths in this tool are relative to the current boilerplate folder, i.e boilerplate/i18n -// This maps the CLI files that exists in a parent folder -const cliRelativePath = BOILERPLATE_PATHS.cli.map(file => `../cli/${file}`); - -BOILERPLATE_PATHS.elements = [...cliRelativePath, 'package.json', 'src/polyfills.ts']; - -BOILERPLATE_PATHS.i18n = [...cliRelativePath, 'angular.json', 'package.json', 'src/polyfills.ts']; - -BOILERPLATE_PATHS['service-worker'] = [...cliRelativePath, 'angular.json', 'package.json']; - -BOILERPLATE_PATHS.testing = [ - ...cliRelativePath, - 'angular.json', - 'tsconfig.app.json', - 'tsconfig.spec.json' -]; - -BOILERPLATE_PATHS.universal = [...cliRelativePath, 'angular.json', 'package.json']; - -BOILERPLATE_PATHS['getting-started'] = [ - ...cliRelativePath, - 'src/styles.css' -]; - -BOILERPLATE_PATHS.schematics = [ - ...cliRelativePath, - 'angular.json' -]; - -BOILERPLATE_PATHS['cli-ajs'] = [ - ...cliRelativePath, - 'package.json' -]; - -BOILERPLATE_PATHS.viewengine = { - systemjs: ['rollup-config.js', 'tsconfig-aot.json'], - cli: ['tsconfig.json'] -}; - const EXAMPLE_CONFIG_FILENAME = 'example-config.json'; class ExampleBoilerPlate { @@ -96,24 +44,26 @@ class ExampleBoilerPlate { const boilerPlateType = exampleConfig.projectType || 'cli'; const boilerPlateBasePath = path.resolve(BOILERPLATE_BASE_PATH, boilerPlateType); - // Copy the boilerplate specific files - BOILERPLATE_PATHS[boilerPlateType].forEach( - filePath => this.copyFile(boilerPlateBasePath, exampleFolder, filePath)); + // All example types other than `cli` and `systemjs` are based on `cli`. Copy over the `cli` + // 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); + } - // Copy the boilerplate common files - const useCommonBoilerplate = exampleConfig.useCommonBoilerplate !== false; + // Copy the type-specific boilerplate files. + this.copyDirectoryContents(boilerPlateBasePath, exampleFolder); - if (useCommonBoilerplate) { - BOILERPLATE_PATHS.common.forEach(filePath => this.copyFile(BOILERPLATE_COMMON_BASE_PATH, exampleFolder, filePath)); + // Copy the common boilerplate files (unless explicitly not used). + if (exampleConfig.useCommonBoilerplate !== false) { + this.copyDirectoryContents(BOILERPLATE_COMMON_PATH, exampleFolder); } // Copy ViewEngine (pre-Ivy) specific files if (viewengine) { const veBoilerPlateType = boilerPlateType === 'systemjs' ? 'systemjs' : 'cli'; - const veBoilerPlateBasePath = - path.resolve(BOILERPLATE_BASE_PATH, 'viewengine', veBoilerPlateType); - BOILERPLATE_PATHS.viewengine[veBoilerPlateType].forEach( - filePath => this.copyFile(veBoilerPlateBasePath, exampleFolder, filePath)); + const veBoilerPlateBasePath = path.resolve(BOILERPLATE_VIEWENGINE_PATH, veBoilerPlateType); + this.copyDirectoryContents(veBoilerPlateBasePath, exampleFolder); } }); } @@ -137,22 +87,28 @@ class ExampleBoilerPlate { return glob.sync(pattern, {ignore: [ignorePattern]}).map(file => path.dirname(file)); } - copyFile(sourceFolder, destinationFolder, filePath) { - const sourcePath = path.resolve(sourceFolder, filePath); - - // normalize path if needed - filePath = this.normalizePath(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}) || {}; } - normalizePath(filePath) { - // transform for example ../cli/src/tsconfig.app.json to src/tsconfig.app.json - return filePath.replace(/\.{2}\/\w+\//, ''); + copyDirectoryContents(srcDir, dstDir) { + shelljs.ls('-Al', srcDir).forEach(stat => { + const srcPath = path.resolve(srcDir, stat.name); + const dstPath = path.resolve(dstDir, stat.name); + + 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); + } + }); } } diff --git a/aio/tools/examples/example-boilerplate.spec.js b/aio/tools/examples/example-boilerplate.spec.js index 4fb314fb08..cff911a1c3 100644 --- a/aio/tools/examples/example-boilerplate.spec.js +++ b/aio/tools/examples/example-boilerplate.spec.js @@ -9,24 +9,13 @@ describe('example-boilerplate tool', () => { describe('add', () => { const sharedDir = path.resolve(__dirname, 'shared'); const sharedNodeModulesDir = path.resolve(sharedDir, 'node_modules'); - const BPFiles = { - cli: 21, - i18n: 3, - universal: 2, - systemjs: 7, - common: 1, - viewengine: { - cli: 1, - systemjs: 2, - }, - }; const exampleFolders = ['a/b', 'c/d']; beforeEach(() => { spyOn(fs, 'ensureSymlinkSync'); spyOn(fs, 'existsSync').and.returnValue(true); spyOn(shelljs, 'exec'); - spyOn(exampleBoilerPlate, 'copyFile'); + spyOn(exampleBoilerPlate, 'copyDirectoryContents'); spyOn(exampleBoilerPlate, 'getFoldersContaining').and.returnValue(exampleFolders); spyOn(exampleBoilerPlate, 'loadJsonFile').and.returnValue({}); }); @@ -61,58 +50,81 @@ describe('example-boilerplate tool', () => { it('should copy all the source boilerplate files for systemjs', () => { const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); - exampleBoilerPlate.loadJsonFile.and.callFake(filePath => filePath.indexOf('a/b') !== -1 ? { projectType: 'systemjs' } : {}); + exampleBoilerPlate.loadJsonFile.and.returnValue({ projectType: 'systemjs' }); + exampleBoilerPlate.add(); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( - (BPFiles.cli) + - (BPFiles.systemjs) + - (BPFiles.common * exampleFolders.length) - ); - // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/systemjs`, 'a/b', 'package.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/common`, 'a/b', 'src/styles.css'); + + 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'], + ]); }); it('should copy all the source boilerplate files for cli', () => { const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); + exampleBoilerPlate.loadJsonFile.and.returnValue({ projectType: 'cli' }); + exampleBoilerPlate.add(); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( - (BPFiles.cli * exampleFolders.length) + - (BPFiles.common * exampleFolders.length) - ); - // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/cli`, 'a/b', 'package.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/common`, 'c/d', 'src/styles.css'); + + 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'], + ]); }); - it('should copy all the source boilerplate files for i18n', () => { + it('should default to `cli` if `projectType` is not specified', () => { const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); - exampleBoilerPlate.loadJsonFile.and.callFake(filePath => filePath.indexOf('a/b') !== -1 ? { projectType: 'i18n' } : {}) + exampleBoilerPlate.loadJsonFile.and.returnValue({}); + exampleBoilerPlate.add(); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( - (BPFiles.cli + BPFiles.i18n) + - (BPFiles.cli) + - (BPFiles.common * exampleFolders.length) - ); - // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/i18n`, 'a/b', '../cli/angular.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/i18n`, 'a/b', 'package.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/common`, 'c/d', 'src/styles.css'); + + 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'], + ]); }); - it('should copy all the source boilerplate files for universal', () => { + it('should copy all the source boilerplate files for i18n (on top of the cli ones)', () => { const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); - exampleBoilerPlate.loadJsonFile.and.callFake(filePath => filePath.indexOf('a/b') !== -1 ? { projectType: 'universal' } : {}) + exampleBoilerPlate.loadJsonFile.and.returnValue({ projectType: 'i18n' }); + exampleBoilerPlate.add(); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( - (BPFiles.cli + BPFiles.universal) + - (BPFiles.cli) + - (BPFiles.common * exampleFolders.length) - ); - // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/universal`, 'a/b', '../cli/tslint.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/universal`, 'a/b', 'angular.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/common`, 'c/d', 'src/styles.css'); + + 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'], + ]); + }); + + it('should copy all the source boilerplate files for universal (on top of the cli ones)', () => { + const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); + exampleBoilerPlate.loadJsonFile.and.returnValue({ projectType: 'universal' }); + + exampleBoilerPlate.add(); + + 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'], + ]); }); it('should try to load the example config file', () => { @@ -130,27 +142,55 @@ describe('example-boilerplate tool', () => { it('should copy all the source boilerplate files for systemjs', () => { const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); - exampleBoilerPlate.loadJsonFile.and.callFake(filePath => filePath.indexOf('a/b') !== -1 ? { projectType: 'systemjs' } : {}); + exampleBoilerPlate.loadJsonFile.and.returnValue({ projectType: 'systemjs' }); + exampleBoilerPlate.add(true); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( - (BPFiles.cli + BPFiles.viewengine.cli) + - (BPFiles.systemjs + BPFiles.viewengine.systemjs) + - (BPFiles.common * exampleFolders.length) - ); - // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/viewengine/systemjs`, 'a/b', 'tsconfig-aot.json'); + + 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'], + ]); }); it('should copy all the source boilerplate files for cli', () => { const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); + exampleBoilerPlate.loadJsonFile.and.returnValue({ projectType: 'cli' }); + exampleBoilerPlate.add(true); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( - (BPFiles.cli * exampleFolders.length) + - (BPFiles.viewengine.cli * exampleFolders.length) + - (BPFiles.common * exampleFolders.length) - ); - // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/viewengine/cli`, 'a/b', 'tsconfig.json'); + + 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'], + ]); + }); + + it('should copy all the source boilerplate files for elements', () => { + const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); + exampleBoilerPlate.loadJsonFile.and.returnValue({ projectType: 'elements' }); + + exampleBoilerPlate.add(true); + + 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'], + ]); }); }); }); @@ -172,16 +212,110 @@ describe('example-boilerplate tool', () => { }); }); - 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('copyDirectoryContents', () => { + const spyFnFor = fnName => (...args) => { callLog.push(`${fnName}(${args.join(', ')})`); }; + let callLog; + + beforeEach(() => { + callLog = []; + spyOn(shelljs, 'chmod').and.callFake(spyFnFor('chmod')); + spyOn(shelljs, 'cp').and.callFake(spyFnFor('cp')); + spyOn(shelljs, 'mkdir').and.callFake(spyFnFor('mkdir')); + spyOn(shelljs, 'test').and.callFake(spyFnFor('test')); + }); + + it('should list all contents of a directory', () => { + const lsSpy = spyOn(shelljs, 'ls').and.returnValue([]); + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); + expect(lsSpy).toHaveBeenCalledWith('-Al', 'source/dir'); + }); + + it('should use 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'); + + expect(callLog).toEqual([ + `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')})`, + + `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}, + {name: 'existing-file.txt', isDirectory: () => false}, + ]); + shelljs.test.and.callFake((_, filePath) => filePath.endsWith('existing-file.txt')); + + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); + + expect(callLog).toEqual([ + `cp(${path.resolve('source/dir/new-file.txt')}, destination/dir)`, + `chmod(444, ${path.resolve('destination/dir/new-file.txt')})`, + + `chmod(666, ${path.resolve('destination/dir/existing-file.txt')})`, + `cp(${path.resolve('source/dir/existing-file.txt')}, destination/dir)`, + `chmod(444, ${path.resolve('destination/dir/existing-file.txt')})`, + ]); + }); + + it('should recursively copy sub-directories', () => { + spyOn(shelljs, 'ls') + .withArgs('-Al', 'source/dir').and.returnValue([ + {name: 'file-1.txt', isDirectory: () => false}, + {name: 'sub-dir-1', isDirectory: () => true}, + {name: 'file-2.txt', isDirectory: () => false}, + ]) + .withArgs('-Al', path.resolve('source/dir/sub-dir-1')).and.returnValue([ + {name: 'file-3.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-4.txt', isDirectory: () => false}, + ]); + + exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); + + 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')})`, + + // Create `sub-dir-1` and recursively copy its contents. + `mkdir(-p, ${path.resolve('destination/dir/sub-dir-1')})`, + + // Copy `sub-dir-1/file-3.txt`. + `test(-f, ${path.resolve('destination/dir/sub-dir-1/file-3.txt')})`, + 'cp(' + + `${path.resolve('source/dir/sub-dir-1/file-3.txt')}, ` + + `${path.resolve('destination/dir/sub-dir-1')})`, + `chmod(444, ${path.resolve('destination/dir/sub-dir-1/file-3.txt')})`, + + // Create `sub-dir-1/sub-dir-2` and recursively copy its contents. + `mkdir(-p, ${path.resolve('destination/dir/sub-dir-1/sub-dir-2')})`, + + // Copy `sub-dir-1/sub-dir-2/file-4.txt`. + `test(-f, ${path.resolve('destination/dir/sub-dir-1/sub-dir-2/file-4.txt')})`, + 'cp(' + + `${path.resolve('source/dir/sub-dir-1/sub-dir-2/file-4.txt')}, ` + + `${path.resolve('destination/dir/sub-dir-1/sub-dir-2')})`, + `chmod(444, ${path.resolve('destination/dir/sub-dir-1/sub-dir-2/file-4.txt')})`, + + // Copy `file-2.txt`. + `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')})`, + ]); }); });