Skip to content

Commit

Permalink
make it better, add licenses too
Browse files Browse the repository at this point in the history
  • Loading branch information
lukekarrys committed Dec 7, 2024
1 parent 25dd6f8 commit 2766e2a
Showing 1 changed file with 133 additions and 71 deletions.
204 changes: 133 additions & 71 deletions scripts/engines.mjs
Original file line number Diff line number Diff line change
@@ -1,86 +1,148 @@
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import subset from 'semver/ranges/subset.js';
import { relative } from 'node:path';
import { relative, join } from 'node:path';

const ENGINES = '>=14';
/**
* An object containing validation checks for package properties.
*
* @constant
* @type {Object}
* @property {Object} engines - Validation check for the "engines" property.
* @property {string} engines.wanted - The desired version range for Node.js engines.
* @property {string} engines.title - The title describing the engines check.
* @property {Function} engines.getValue - Function to get the Node.js engine version from the package.
* @property {Function} engines.isOK - Function to check if the Node.js engine version is acceptable.
*
* @property {Object} licenses - Validation check for the "license" property.
* @property {string[]} licenses.wanted - The list of acceptable licenses.
* @property {string} licenses.title - The title describing the license check.
* @property {Function} licenses.getValue - Function to get the license from the package.
* @property {Function} licenses.isOK - Function to check if the license is acceptable.
*/
const CHECKS = {
engines: {
wanted: '>=14',
get title() {
return `Engines must be a subset of "${this.wanted}"`;
},
getValue(pkg) {
return pkg.engines?.node;
},
isOK(value) {
return value === undefined || subset(this.wanted, value);
},
},
licenses: {
wanted: [
'0BSD',
'BlueOak-1.0.0',
'BSD-2-Clause',
'BSD-3-Clause',
'ISC',
'MIT',
],
get title() {
return `License must be one of the following: "${this.wanted.join(
', '
)}"`;
},
getValue(pkg) {
return pkg.license;
},
isOK(value) {
return value === undefined || this.wanted.includes(value);
},
},
};

/**
* Asynchronously retrieves a sorted list of production dependencies for the current project.
*
* This function uses `pnpm` to list all production dependencies recursively and then processes
* the output to create an array of dependency objects. Each object contains the path to the
* package.json file, the relative path from the current working directory, and the parsed
* package.json content.
*
* @constant {Promise<Array<{path: string, relPath: string, pkg: Object}>>>} DEPS - A promise that resolves to an array of dependency objects.
*
* Each dependency object has the following properties:
* - `path` {string}: The absolute path to the package.json file.
* - `relPath` {string}: The relative path from the current working directory to the package.json file.
* - `pkg` {Object}: The parsed content of the package.json file.
*/
const DEPS = await (async () => {
const pkgs = new Set();

const getAllProdDeps = async () => {
const deps = new Set();
const walk = (dep) => {
if (dep.private) return;
if (dep.path) {
deps.add(dep.path);
}
const children = Array.isArray(dep)
if (dep.path) pkgs.add(join(dep.path, 'package.json'));
for (const child of Array.isArray(dep)
? dep
: Object.values(dep.dependencies ?? {});
for (const child of children) {
: Object.values(dep.dependencies ?? {})) {
walk(child);
}
};
const workspaces = await new Promise((res, rej) => {
const proc = spawn(
'pnpm',
['list', '--recursive', '--depth=Infinity', '--json', '--prod'],
{
cwd: process.cwd(),
shell: true,
}
);
let output = '';
proc.stdout.on('data', (data) => (output += data.toString()));
proc.on('close', () => res(JSON.parse(output))).on('error', rej);
});
for (const ws of workspaces) {
walk(ws);
}
return [...deps].map((path) => ({
path,
pkg: JSON.parse(readFileSync(resolve(path, 'package.json'), 'utf8')),
}));
};

const check = (key, packages, value, ok) => {
const problems = packages.filter((p) => !ok(value(p)));
if (problems.length) {
return [
`The following dependencies ${key} problems were found:`,
...problems.map((p) =>
[
`${p.pkg.name}@${p.pkg.version}`,
` ${key}: ${value(p)}`,
` path: ${relative(process.cwd(), p.path)}/package.json`,
].join('\n')
),
].join('\n\n');
}
};

const main = async () => {
const deps = await getAllProdDeps();
const checkEngines = check(
'engines',
deps,
(d) => d.pkg.engines?.node,
(v) => v === undefined || subset(ENGINES, v)
walk(
await new Promise((res, rej) => {
const proc = spawn(
'pnpm',
['list', '--recursive', '--depth=Infinity', '--json', '--prod'],
{
cwd: process.cwd(),
shell: true,
}
);
let output = '';
proc.stdout.on('data', (data) => (output += data.toString()));
proc.on('close', () => res(JSON.parse(output))).on('error', rej);
})
);
if (checkEngines) {
throw new Error(checkEngines);

return [...pkgs]
.map((path) => ({
path,
relPath: relative(process.cwd(), path),
pkg: JSON.parse(readFileSync(path, 'utf8')),
}))
.sort((a, b) => a.pkg.name.localeCompare(b.pkg.name, 'en'));
})();

const problems = (
Object.hasOwn(CHECKS, process.argv[2])
? [CHECKS[process.argv[2]]]
: Object.values(CHECKS)
)
.map((check) => {
const problems = DEPS.filter(
(dep) => !check.isOK(check.getValue(dep.pkg))
);
if (!problems.length) return null;
return { deps: problems, problem: check };
})
.filter((v) => v !== null);

if (problems.length) {
for (const { deps, problem } of problems) {
console.group(problem.title);
for (const { pkg, relPath } of deps) {
console.group(`${pkg.name}@${pkg.version}`);
console.log(`found: "${problem.getValue(pkg)}"`);
console.log(relPath);
console.groupEnd();
}
console.groupEnd();
console.log('');
}
return (
`Successfully checked ${deps.length} production dependencies:\n` +
deps
.map((d) => `${d.pkg.name}@${d.pkg.version} `)
.sort()
.join('\n')
);
};
console.log('not ok');
process.exit(1);
}

main()
.then(console.log)
.catch((e) => {
process.exitCode = 1;
console.error(e.message);
});
for (const { pkg, relPath } of DEPS) {
console.group(`${pkg.name}@${pkg.version}`);
console.log(relPath);
console.groupEnd();
}
console.log('');
console.log('ok');

0 comments on commit 2766e2a

Please sign in to comment.