Skip to content

Instantly share code, notes, and snippets.

@cardoso
Created July 15, 2025 17:50
Show Gist options
  • Select an option

  • Save cardoso/76a2d7dac88c0f2894d2a1ad9b8fa8fd to your computer and use it in GitHub Desktop.

Select an option

Save cardoso/76a2d7dac88c0f2894d2a1ad9b8fa8fd to your computer and use it in GitHub Desktop.
check-package-json.ts
import type { Dirent } from 'node:fs';
import { readFile, writeFile, glob, readdir } from 'node:fs/promises';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dirname, '..');
const PACKAGE_JSON = 'package.json';
function parseJson(text: string): unknown {
return JSON.parse(text);
}
function isObject(value: unknown) {
return typeof value === 'object' && value !== null;
}
function isArray(value: unknown): value is unknown[] {
return Array.isArray(value);
}
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function getWorkspaces(content: string) {
const json = parseJson(content);
if (typeof json !== 'object') {
throw new Error('Package.json is not an object');
}
if (json === null) {
throw new Error('Package.json is null');
}
if (!('workspaces' in json)) {
throw new Error('Package.json does not contain "workspaces"');
}
const { workspaces } = json;
if (!isArray(workspaces)) {
throw new Error('Package.json "workspaces" is not an array');
}
if (workspaces.length === 0) {
throw new Error('Package.json "workspaces" is an empty array');
}
if (!workspaces.every(isString)) {
throw new Error('Package.json "workspaces" contains non-string values');
}
return workspaces;
}
class PackageVisitor {
#patterns: AsyncIteratorObject<Dirent, void, unknown>;
constructor(patterns: string[]) {
this.#patterns = glob(patterns, { cwd: ROOT, withFileTypes: true });
}
async *[Symbol.asyncIterator]() {
for await (const dirent of this.#patterns) {
const fullPath = path.join(dirent.parentPath, dirent.name);
const children = await readdir(fullPath, { withFileTypes: true, encoding: 'utf8' });
yield { dirent, children };
}
}
}
async function fetchWorkspaces(): Promise<PackageVisitor> {
const content = await readFile(PACKAGE_JSON, 'utf8');
return new PackageVisitor(getWorkspaces(content));
}
type DeepPartial<T> = { [P in keyof T]?: T[P] extends Record<string, unknown> ? DeepPartial<T[P]> : T[P] } & {};
type PackageJson = {
name: string;
version: string;
private: boolean;
devDependencies: Record<string, string>;
scripts: Record<string, string>;
main: string;
typings: string;
files: string[];
volta: {
extends: string;
};
};
class RuleContext {
#relative: string;
#fix: boolean;
constructor(relative: string, fix: boolean) {
this.#relative = relative;
this.#fix = fix;
}
get relative() {
return this.#relative;
}
get fix() {
return this.#fix;
}
error(message: string): false {
console.error(`[${this.#relative}] ${message}`);
return false;
}
}
interface Rule<T> {
name: string;
(this: RuleContext, value: DeepPartial<T>): boolean | Promise<boolean>;
}
const rules = [
function name(json) {
return 'name' in json && typeof json.name === 'string';
},
function scope(json) {
return json.name?.startsWith('@rocket.chat/') ?? false;
},
async function volta(json) {
if (
!(
'volta' in json &&
typeof json.volta === 'object' &&
json.volta !== null &&
'extends' in json.volta &&
typeof json.volta.extends === 'string'
)
) {
return false;
}
const extendsPath = path.join(this.relative, json.volta.extends);
if (extendsPath === 'package.json') {
return true;
}
if (this.fix) {
// Fix the extends path
json.volta.extends = path.relative(this.relative, PACKAGE_JSON);
await writeFile(path.join(this.relative, PACKAGE_JSON), JSON.stringify(json, null, '\t') + '\n', 'utf8');
console.log(`Fixed "volta.extends" in ${this.relative}`);
return true;
}
return this.error(path.join(this.relative, json.volta.extends));
},
] as Rule<PackageJson>[];
async function checkPackageJson() {
const workspaces = await fetchWorkspaces();
const fix = process.argv.includes('--fix');
for await (const { dirent, children } of workspaces) {
try {
for (const child of children) {
const relative = path.relative(ROOT, child.parentPath);
const context = new RuleContext(relative, fix);
if (child.name === PACKAGE_JSON) {
const content = await readFile(path.join(child.parentPath, child.name), 'utf8');
const json = parseJson(content);
if (!isObject(json)) {
throw new Error(`Invalid JSON in ${child.name}`);
}
for (const rule of rules) {
await rule.apply(context, [json]);
}
// console.dir(packageJson);
}
}
} catch (error) {
console.error('Error reading package.json:', error);
}
}
}
checkPackageJson();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment