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
This commit is contained in:
George Kalpakas 2020-09-27 13:37:43 +03:00 committed by Alex Rickabaugh
parent 37ed4bd9ff
commit f6052a915d
4 changed files with 120 additions and 69 deletions

View File

@ -32,14 +32,11 @@ Any necessary changes to boilerplate files will be done automatically through mi
# Migrate project to new versions. # Migrate project to new versions.
yarn ng update @angular/cli --migrate-only --from=<previous-cli-version> yarn ng update @angular/cli --migrate-only --from=<previous-cli-version>
yarn ng update @angular/core --migrate-only --from=<previous-core-version> yarn ng update @angular/core --migrate-only --from=<previous-core-version>
# Remove `node_modules/` and `yarn.lock`.
rm -rf node_modules yarn.lock
``` ```
> NOTE: > 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. > 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. - 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. Inspect the changes in `cli/` and manually apply the necessary ones to other CLI-based boilerplate folders.

View File

@ -1,5 +1,6 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const glob = require('glob'); const glob = require('glob');
const ignore = require('ignore');
const path = require('canonical-path'); const path = require('canonical-path');
const shelljs = require('shelljs'); const shelljs = require('shelljs');
const yargs = require('yargs'); 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 // Get all the examples folders, indicated by those that contain a `example-config.json` file
const exampleFolders = const exampleFolders =
this.getFoldersContaining(EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, 'node_modules'); 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)) { if (!fs.existsSync(SHARED_NODE_MODULES_PATH)) {
throw new Error( throw new Error(
@ -48,22 +51,22 @@ class ExampleBoilerPlate {
// boilerplate files first. // boilerplate files first.
// (Some of these files might be later overwritten by type-specific files.) // (Some of these files might be later overwritten by type-specific files.)
if (boilerPlateType !== 'cli' && boilerPlateType !== 'systemjs') { if (boilerPlateType !== 'cli' && boilerPlateType !== 'systemjs') {
this.copyDirectoryContents(BOILERPLATE_CLI_PATH, exampleFolder); this.copyDirectoryContents(BOILERPLATE_CLI_PATH, exampleFolder, isPathIgnored);
} }
// Copy the type-specific boilerplate files. // Copy the type-specific boilerplate files.
this.copyDirectoryContents(boilerPlateBasePath, exampleFolder); this.copyDirectoryContents(boilerPlateBasePath, exampleFolder, isPathIgnored);
// Copy the common boilerplate files (unless explicitly not used). // Copy the common boilerplate files (unless explicitly not used).
if (exampleConfig.useCommonBoilerplate !== false) { if (exampleConfig.useCommonBoilerplate !== false) {
this.copyDirectoryContents(BOILERPLATE_COMMON_PATH, exampleFolder); this.copyDirectoryContents(BOILERPLATE_COMMON_PATH, exampleFolder, isPathIgnored);
} }
// Copy ViewEngine (pre-Ivy) specific files // Copy ViewEngine (pre-Ivy) specific files
if (viewengine) { if (viewengine) {
const veBoilerPlateType = boilerPlateType === 'systemjs' ? 'systemjs' : 'cli'; const veBoilerPlateType = boilerPlateType === 'systemjs' ? 'systemjs' : 'cli';
const veBoilerPlateBasePath = path.resolve(BOILERPLATE_VIEWENGINE_PATH, veBoilerPlateType); 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}) || {}; } loadJsonFile(filePath) { return fs.readJsonSync(filePath, {throws: false}) || {}; }
copyDirectoryContents(srcDir, dstDir) { copyDirectoryContents(srcDir, dstDir, isPathIgnored) {
shelljs.ls('-Al', srcDir).forEach(stat => { shelljs.ls('-Al', srcDir).forEach(stat => {
const srcPath = path.resolve(srcDir, stat.name); const srcPath = path.resolve(srcDir, stat.name);
const dstPath = path.resolve(dstDir, 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()) { if (stat.isDirectory()) {
// `srcPath` is a directory: Recursively copy it to `dstDir`. // `srcPath` is a directory: Recursively copy it to `dstDir`.
shelljs.mkdir('-p', dstPath); shelljs.mkdir('-p', dstPath);
return this.copyDirectoryContents(srcPath, dstPath); return this.copyDirectoryContents(srcPath, dstPath, isPathIgnored);
} 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);
} }
// `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

@ -56,10 +56,10 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/systemjs`, 'a/b'], [`${boilerplateDir}/systemjs`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/systemjs`, 'c/d'], [`${boilerplateDir}/systemjs`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
@ -71,10 +71,10 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/cli`, 'a/b'], [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/cli`, 'c/d'], [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
@ -86,10 +86,10 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(4);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/cli`, 'a/b'], [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/cli`, 'c/d'], [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
@ -101,12 +101,12 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/cli`, 'a/b'], [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/i18n`, 'a/b'], [`${boilerplateDir}/i18n`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/cli`, 'c/d'], [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/i18n`, 'c/d'], [`${boilerplateDir}/i18n`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
@ -118,12 +118,12 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/cli`, 'a/b'], [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/universal`, 'a/b'], [`${boilerplateDir}/universal`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/cli`, 'c/d'], [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/universal`, 'c/d'], [`${boilerplateDir}/universal`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
@ -148,12 +148,12 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/systemjs`, 'a/b'], [`${boilerplateDir}/systemjs`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/viewengine/systemjs`, 'a/b'], [`${boilerplateDir}/viewengine/systemjs`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/systemjs`, 'c/d'], [`${boilerplateDir}/systemjs`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/viewengine/systemjs`, 'c/d'], [`${boilerplateDir}/viewengine/systemjs`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
@ -165,12 +165,12 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(6);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/cli`, 'a/b'], [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/viewengine/cli`, 'a/b'], [`${boilerplateDir}/viewengine/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/cli`, 'c/d'], [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/viewengine/cli`, 'c/d'], [`${boilerplateDir}/viewengine/cli`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
@ -182,14 +182,14 @@ describe('example-boilerplate tool', () => {
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(8); expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(8);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([ expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/cli`, 'a/b'], [`${boilerplateDir}/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/elements`, 'a/b'], [`${boilerplateDir}/elements`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'a/b'], [`${boilerplateDir}/common`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/viewengine/cli`, 'a/b'], [`${boilerplateDir}/viewengine/cli`, 'a/b', jasmine.any(Function)],
[`${boilerplateDir}/cli`, 'c/d'], [`${boilerplateDir}/cli`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/elements`, 'c/d'], [`${boilerplateDir}/elements`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/common`, 'c/d'], [`${boilerplateDir}/common`, 'c/d', jasmine.any(Function)],
[`${boilerplateDir}/viewengine/cli`, 'c/d'], [`${boilerplateDir}/viewengine/cli`, 'c/d', jasmine.any(Function)],
]); ]);
}); });
}); });
@ -214,10 +214,12 @@ describe('example-boilerplate tool', () => {
describe('copyDirectoryContents', () => { describe('copyDirectoryContents', () => {
const spyFnFor = fnName => (...args) => { callLog.push(`${fnName}(${args.join(', ')})`); }; const spyFnFor = fnName => (...args) => { callLog.push(`${fnName}(${args.join(', ')})`); };
let isPathIgnoredSpy;
let callLog; let callLog;
beforeEach(() => { beforeEach(() => {
callLog = []; callLog = [];
isPathIgnoredSpy = jasmine.createSpy('isPathIgnored').and.returnValue(false);
spyOn(shelljs, 'chmod').and.callFake(spyFnFor('chmod')); spyOn(shelljs, 'chmod').and.callFake(spyFnFor('chmod'));
spyOn(shelljs, 'cp').and.callFake(spyFnFor('cp')); spyOn(shelljs, 'cp').and.callFake(spyFnFor('cp'));
spyOn(shelljs, 'mkdir').and.callFake(spyFnFor('mkdir')); spyOn(shelljs, 'mkdir').and.callFake(spyFnFor('mkdir'));
@ -226,17 +228,17 @@ describe('example-boilerplate tool', () => {
it('should list all contents of a directory', () => { it('should list all contents of a directory', () => {
const lsSpy = spyOn(shelljs, 'ls').and.returnValue([]); 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'); 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([ spyOn(shelljs, 'ls').and.returnValue([
{name: 'file-1.txt', isDirectory: () => false}, {name: 'file-1.txt', isDirectory: () => false},
{name: 'file-2.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([ expect(callLog).toEqual([
`test(-f, ${path.resolve('destination/dir/file-1.txt')})`, `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', () => { it('should make existing files in destination writable before overwriting', () => {
spyOn(shelljs, 'ls').and.returnValue([ spyOn(shelljs, 'ls').and.returnValue([
{name: 'new-file.txt', isDirectory: () => false}, {name: 'new-file.txt', isDirectory: () => false},
@ -256,7 +274,7 @@ describe('example-boilerplate tool', () => {
]); ]);
shelljs.test.and.callFake((_, filePath) => filePath.endsWith('existing-file.txt')); 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([ expect(callLog).toEqual([
`cp(${path.resolve('source/dir/new-file.txt')}, destination/dir)`, `cp(${path.resolve('source/dir/new-file.txt')}, destination/dir)`,
@ -283,7 +301,7 @@ describe('example-boilerplate tool', () => {
{name: 'file-4.txt', isDirectory: () => false}, {name: 'file-4.txt', isDirectory: () => false},
]); ]);
exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir'); exampleBoilerPlate.copyDirectoryContents('source/dir', 'destination/dir', isPathIgnoredSpy);
expect(callLog).toEqual([ expect(callLog).toEqual([
// Copy `file-1.txt`. // Copy `file-1.txt`.
@ -317,6 +335,33 @@ describe('example-boilerplate tool', () => {
`chmod(444, ${path.resolve('destination/dir/file-2.txt')})`, `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', () => { describe('loadJsonFile', () => {

View File

@ -1,2 +1,3 @@
**/node_modules/
**/package-lock.json
**/yarn.lock **/yarn.lock
**/package-lock.json