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
This commit is contained in:
George Kalpakas 2020-07-22 14:40:03 +03:00 committed by Andrew Kushnir
parent 65da87722d
commit 5a733629b5
2 changed files with 247 additions and 157 deletions

View File

@ -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);
}
});
}
}

View File

@ -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')})`,
]);
});
});