tools/pkglint/lib/util.ts (127 lines of code) (raw):
import * as fs from 'fs';
import * as path from 'path';
import { PackageJson, PKGLINT_IGNORES } from './packagejson';
/**
* Expect a particular JSON key to be a given value
*/
export function expectJSON(ruleName: string, pkg: PackageJson, jsonPath: string, expected: any, ignore?: RegExp, caseInsensitive: boolean = false) {
const parts = jsonPath.split('.');
const actual = deepGet(pkg.json, parts);
if (applyCaseInsensitive(applyIgnore(actual)) !== applyCaseInsensitive(applyIgnore(expected))) {
pkg.report({
ruleName,
message: `${jsonPath} should be ${JSON.stringify(expected)}${ignore ? ` (ignoring ${ignore})` : ''}, is ${JSON.stringify(actual)}`,
fix: () => { deepSet(pkg.json, parts, expected); },
});
}
function applyIgnore(val: any): string {
if (!ignore || val == null) { return JSON.stringify(val); }
const str = JSON.stringify(val);
return str.replace(ignore, '');
}
function applyCaseInsensitive(val: any): string {
if (!caseInsensitive || val == null) { return JSON.stringify(val); }
const str = JSON.stringify(val);
return str.toLowerCase();
}
}
/**
* Export a package-level file to contain a given line
*/
export function fileShouldContain(ruleName: string, pkg: PackageJson, fileName: string, ...lines: string[]) {
for (const line of lines) {
const doesContain = pkg.fileContainsSync(fileName, line);
if (!doesContain) {
pkg.report({
ruleName,
message: `${fileName} should contain '${line}'`,
fix: () => pkg.addToFileSync(fileName, line),
});
}
}
}
export function fileShouldNotContain(ruleName: string, pkg: PackageJson, fileName: string, ...lines: string[]) {
for (const line of lines) {
const doesContain = pkg.fileContainsSync(fileName, line);
if (doesContain) {
pkg.report({
ruleName,
message: `${fileName} should NOT contain '${line}'`,
fix: () => pkg.removeFromFileSync(fileName, line),
});
}
}
}
/**
* Export a package-level file to contain specific content
*/
export function fileShouldBe(ruleName: string, pkg: PackageJson, fileName: string, content: string) {
const isContent = pkg.fileIsSync(fileName, content);
if (!isContent) {
pkg.report({
ruleName,
message: `${fileName} should contain exactly '${content}'`,
fix: () => pkg.writeFileSync(fileName, content),
});
}
}
/**
* Enforce a dev dependency
*/
export function expectDevDependency(ruleName: string, pkg: PackageJson, packageName: string, version: string) {
const actualVersion = pkg.getDevDependency(packageName);
if (version !== actualVersion) {
pkg.report({
ruleName,
message: `Missing devDependency: ${packageName} @ ${version}`,
fix: () => pkg.addDevDependency(packageName, version),
});
}
}
/**
* Return whether the given value is an object
*
* Even though arrays technically are objects, we usually want to treat them differently,
* so we return false in those cases.
*/
export function isObject(x: any) {
return x !== null && typeof x === 'object' && !Array.isArray(x);
}
/**
* Deep get a value from a tree of nested objects
*
* Returns undefined if any part of the path was unset or
* not an object.
*/
export function deepGet(x: any, jsonPath: string[]): any {
jsonPath = jsonPath.slice();
while (jsonPath.length > 0 && isObject(x)) {
const key = jsonPath.shift()!;
x = x[key];
}
return jsonPath.length === 0 ? x : undefined;
}
/**
* Deep set a value in a tree of nested objects
*
* Throws an error if any part of the path is not an object.
*/
export function deepSet(x: any, jsonPath: string[], value: any) {
jsonPath = jsonPath.slice();
if (jsonPath.length === 0) {
throw new Error('Path may not be empty');
}
while (jsonPath.length > 1 && isObject(x)) {
const key = jsonPath.shift()!;
if (!(key in x)) { x[key] = {}; }
x = x[key];
}
if (!isObject(x)) {
throw new Error(`Expected an object, got '${x}'`);
}
x[jsonPath[0]] = value;
}
export function findUpward(dir: string, pred: (x: string) => boolean): string | undefined {
while (true) {
if (pred(dir)) { return dir; }
const parent = path.dirname(dir);
if (parent === dir) {
return undefined;
}
dir = parent;
}
}
export function monoRepoRoot() {
const ret = findUpward(process.cwd(), d => fs.existsSync(path.join(d, 'lerna.json')) || fs.existsSync(path.join(d, '.nzmroot')));
if (!ret) {
throw new Error('Could not find lerna.json');
}
return ret;
}
export function* findInnerPackages(dir: string): IterableIterator<string> {
for (const fname of fs.readdirSync(dir, { encoding: 'utf8' })) {
try {
const stat = fs.statSync(path.join(dir, fname));
if (!stat.isDirectory()) { continue; }
} catch (e: any) {
// Survive invalid symlinks
if (e.code !== 'ENOENT') { throw e; }
continue;
}
if (PKGLINT_IGNORES.includes(fname)) { continue; }
if (fs.existsSync(path.join(dir, fname, 'package.json'))) {
yield path.join(dir, fname);
}
yield* findInnerPackages(path.join(dir, fname));
}
}