2019-09-10 06:47:36 -04:00
|
|
|
#!/usr/bin/env node
|
2019-08-27 17:39:13 -04:00
|
|
|
'use strict';
|
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
/**
|
|
|
|
* **Usage:**
|
|
|
|
* ```
|
|
|
|
* node aio/scripts/verify-codeownership
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* Verify whether there are directories in the codebase that don't have a codeowner (in `.github/CODEOWNERS`) and vice
|
|
|
|
* versa (that there are no patterns in `CODEOWNERS` that do not correspond to actual directories).
|
|
|
|
*
|
|
|
|
* The script does not aim to be exhaustive and highly accurate, checking all files and directories (since that would be
|
|
|
|
* too complicated). Instead, it does a coarse check on some important (or frequently changing) directories.
|
|
|
|
*
|
|
|
|
* Currently, it checks the following:
|
|
|
|
* - **Packages**: Top-level directories in `packages/`.
|
|
|
|
* - **API docs examples**: Top-level directories in `packages/examples/`.
|
|
|
|
* - **Guides**: Top-level files in `aio/content/guide/`.
|
|
|
|
* - **Guide images**: Top-level directories in `aio/content/images/guide/`.
|
|
|
|
* - **Guide examples**: Top-level directories in `aio/content/examples/`.
|
|
|
|
*/
|
|
|
|
|
2019-08-27 17:39:13 -04:00
|
|
|
// Imports
|
|
|
|
const fs = require('fs');
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
// Constants
|
|
|
|
const PROJECT_ROOT_DIR = path.resolve(__dirname, '../..');
|
|
|
|
const CODEOWNERS_PATH = path.resolve(PROJECT_ROOT_DIR, '.github/CODEOWNERS');
|
2019-09-10 06:47:36 -04:00
|
|
|
const PKG_DIR = path.resolve(PROJECT_ROOT_DIR, 'packages');
|
|
|
|
const PKG_EXAMPLES_DIR = path.resolve(PKG_DIR, 'examples');
|
2019-08-27 17:39:13 -04:00
|
|
|
const AIO_CONTENT_DIR = path.resolve(PROJECT_ROOT_DIR, 'aio/content');
|
|
|
|
const AIO_GUIDES_DIR = path.resolve(AIO_CONTENT_DIR, 'guide');
|
|
|
|
const AIO_GUIDE_IMAGES_DIR = path.resolve(AIO_CONTENT_DIR, 'images/guide');
|
|
|
|
const AIO_GUIDE_EXAMPLES_DIR = path.resolve(AIO_CONTENT_DIR, 'examples');
|
2019-09-10 06:47:36 -04:00
|
|
|
const IGNORED_PKG_DIRS = new Set([
|
|
|
|
// Examples are checked separately.
|
|
|
|
'examples',
|
|
|
|
]);
|
2019-08-27 17:39:13 -04:00
|
|
|
|
|
|
|
// Run
|
|
|
|
_main();
|
|
|
|
|
|
|
|
// Functions - Definitions
|
|
|
|
function _main() {
|
2019-09-10 06:47:36 -04:00
|
|
|
const {packages: pkgPackagePaths, examples: pkgExamplePaths} = getPathsFromPkg();
|
2019-08-27 17:39:13 -04:00
|
|
|
const {guides: aioGuidePaths, images: aioGuideImagesPaths, examples: aioExamplePaths} = getPathsFromAioContent();
|
|
|
|
const {
|
2019-09-10 06:47:36 -04:00
|
|
|
pkgPackages: coPkgPackagePaths,
|
|
|
|
pkgExamples: coPkgExamplePaths,
|
2019-08-27 17:39:13 -04:00
|
|
|
aioGuides: coAioGuidePaths,
|
|
|
|
aioImages: coAioGuideImagesPaths,
|
|
|
|
aioExamples: coAioExamplePaths,
|
|
|
|
} = getPathsFromCodeowners();
|
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
const pkgPackagesDiff = arrayDiff(pkgPackagePaths, coPkgPackagePaths);
|
|
|
|
const pkgExamplesDiff = arrayDiff(pkgExamplePaths, coPkgExamplePaths);
|
2019-08-27 17:39:13 -04:00
|
|
|
const aioGuidesDiff = arrayDiff(aioGuidePaths, coAioGuidePaths);
|
|
|
|
const aioImagesDiff = arrayDiff(aioGuideImagesPaths, coAioGuideImagesPaths);
|
|
|
|
const aioExamplesDiff = arrayDiff(aioExamplePaths, coAioExamplePaths);
|
2019-09-10 06:47:36 -04:00
|
|
|
const hasDiff = (pkgPackagesDiff.diffCount > 0) || (pkgExamplesDiff.diffCount > 0) ||
|
|
|
|
(aioGuidesDiff.diffCount > 0) || (aioImagesDiff.diffCount> 0) || (aioExamplesDiff.diffCount > 0);
|
2019-08-27 17:39:13 -04:00
|
|
|
|
|
|
|
if (hasDiff) {
|
2019-09-10 06:47:36 -04:00
|
|
|
const expectedPkgPackagesSrc = path.relative(PROJECT_ROOT_DIR, PKG_DIR);
|
|
|
|
const expectedPkgExamplesSrc = path.relative(PROJECT_ROOT_DIR, PKG_EXAMPLES_DIR);
|
2019-08-27 17:39:13 -04:00
|
|
|
const expectedAioGuidesSrc = path.relative(PROJECT_ROOT_DIR, AIO_GUIDES_DIR);
|
|
|
|
const expectedAioImagesSrc = path.relative(PROJECT_ROOT_DIR, AIO_GUIDE_IMAGES_DIR);
|
|
|
|
const expectedAioExamplesSrc = path.relative(PROJECT_ROOT_DIR, AIO_GUIDE_EXAMPLES_DIR);
|
|
|
|
const actualSrc = path.relative(PROJECT_ROOT_DIR, CODEOWNERS_PATH);
|
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
reportDiff(pkgPackagesDiff, expectedPkgPackagesSrc, actualSrc);
|
|
|
|
reportDiff(pkgExamplesDiff, expectedPkgExamplesSrc, actualSrc);
|
2019-08-27 17:39:13 -04:00
|
|
|
reportDiff(aioGuidesDiff, expectedAioGuidesSrc, actualSrc);
|
|
|
|
reportDiff(aioImagesDiff, expectedAioImagesSrc, actualSrc);
|
|
|
|
reportDiff(aioExamplesDiff, expectedAioExamplesSrc, actualSrc);
|
|
|
|
}
|
|
|
|
|
|
|
|
process.exit(hasDiff ? 1 : 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
function arrayDiff(expected, actual) {
|
|
|
|
const missing = expected.filter(x => !actual.includes(x)).sort();
|
|
|
|
const extra = actual.filter(x => !expected.includes(x)).sort();
|
|
|
|
|
|
|
|
return {missing, extra, diffCount: missing.length + extra.length};
|
|
|
|
}
|
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
function findDirectories(parentDir) {
|
|
|
|
return fs.readdirSync(parentDir).
|
|
|
|
filter(name => fs.statSync(`${parentDir}/${name}`).isDirectory());
|
|
|
|
}
|
|
|
|
|
2019-08-27 17:39:13 -04:00
|
|
|
function getPathsFromAioContent() {
|
|
|
|
return {
|
|
|
|
guides: fs.readdirSync(AIO_GUIDES_DIR),
|
|
|
|
images: fs.readdirSync(AIO_GUIDE_IMAGES_DIR),
|
|
|
|
examples: fs.readdirSync(AIO_GUIDE_EXAMPLES_DIR).
|
|
|
|
filter(name => fs.statSync(`${AIO_GUIDE_EXAMPLES_DIR}/${name}`).isDirectory()),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPathsFromCodeowners() {
|
2019-09-10 06:47:36 -04:00
|
|
|
const pkgPackagesPathRe = /^\/packages\/([^\s\*/]+)\/\*?\*\s/;
|
|
|
|
const pkgExamplesPathRe = /^\/packages\/examples\/([^\s\*/]+)/;
|
2019-08-27 17:39:13 -04:00
|
|
|
// Use capturing groups for `images/` and `examples` to be able to differentiate between the
|
|
|
|
// different kinds of matches (guide, image, example) later (see `isImage`/`isExample` below).
|
2019-09-10 06:47:36 -04:00
|
|
|
const aioGuidesImagesExamplesPathRe = /^\/aio\/content\/(?:(images\/)?guide|(examples))\/([^\s\*/]+)/;
|
2019-08-28 03:13:57 -04:00
|
|
|
const manualGlobExpansions = {
|
|
|
|
// `CODEOWNERS` has a glob to match all `testing/` directories, so no specific glob for
|
|
|
|
// `packages/examples/testing/` is necessary.
|
|
|
|
'testing/**': ['/packages/examples/testing/**'],
|
|
|
|
};
|
2019-08-27 17:39:13 -04:00
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
const pkgPackages = [];
|
|
|
|
const pkgExamples = [];
|
2019-08-27 17:39:13 -04:00
|
|
|
const aioGuides = [];
|
|
|
|
const aioImages = [];
|
|
|
|
const aioExamples = [];
|
|
|
|
|
|
|
|
// Read `CODEOWNERS` and split into lines.
|
|
|
|
const lines = fs.
|
|
|
|
readFileSync(CODEOWNERS_PATH, 'utf8').
|
|
|
|
split('\n').
|
|
|
|
map(l => l.trim());
|
|
|
|
|
2019-08-28 03:13:57 -04:00
|
|
|
// Manually expand globs to known matching patterns.
|
|
|
|
for (const [glob, expansions] of Object.entries(manualGlobExpansions)) {
|
|
|
|
const matchingLine = lines.find(l => l.startsWith(`${glob} `));
|
|
|
|
if (matchingLine !== undefined) {
|
|
|
|
lines.push(...expansions);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
// Collect packages (`packages/`).
|
|
|
|
lines.
|
|
|
|
map(l => l.match(pkgPackagesPathRe)).
|
|
|
|
filter(m => m).
|
|
|
|
forEach(([, path]) => pkgPackages.push(path));
|
|
|
|
|
|
|
|
// Collect API docs examples (`packages/examples/`).
|
|
|
|
lines.
|
|
|
|
map(l => l.match(pkgExamplesPathRe)).
|
|
|
|
filter(m => m).
|
|
|
|
forEach(([, path]) => pkgExamples.push(path));
|
|
|
|
|
2019-08-27 17:39:13 -04:00
|
|
|
// Collect `aio/` guides/images/examples.
|
|
|
|
lines.
|
2019-09-10 06:47:36 -04:00
|
|
|
map(l => l.match(aioGuidesImagesExamplesPathRe)).
|
2019-08-27 17:39:13 -04:00
|
|
|
filter(m => m).
|
|
|
|
forEach(([, isImage, isExample, path]) => {
|
|
|
|
const list = isExample ? aioExamples :
|
|
|
|
isImage ? aioImages :
|
|
|
|
aioGuides;
|
|
|
|
list.push(path);
|
|
|
|
});
|
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
return {pkgPackages, pkgExamples, aioGuides, aioImages, aioExamples};
|
2019-08-27 17:39:13 -04:00
|
|
|
}
|
|
|
|
|
2019-09-10 06:47:36 -04:00
|
|
|
function getPathsFromPkg() {
|
2019-08-27 17:39:13 -04:00
|
|
|
return {
|
2019-09-10 06:47:36 -04:00
|
|
|
packages: findDirectories(PKG_DIR).filter(name => !IGNORED_PKG_DIRS.has(name)),
|
|
|
|
examples: findDirectories(PKG_EXAMPLES_DIR),
|
2019-08-27 17:39:13 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function reportDiff(diff, expectedSrc, actualSrc) {
|
|
|
|
if (diff.missing.length) {
|
|
|
|
console.error(
|
|
|
|
`\nEntries in '${expectedSrc}' but not in '${actualSrc}':\n` +
|
|
|
|
diff.missing.map(x => ` - ${x}`).join('\n'));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (diff.extra.length) {
|
|
|
|
console.error(
|
|
|
|
`\nEntries in '${actualSrc}' but not in '${expectedSrc}':\n` +
|
|
|
|
diff.extra.map(x => ` - ${x}`).join('\n'));
|
|
|
|
}
|
|
|
|
}
|