chore: scaffold toolchain for the Electron shell
Set up the package, compiler and quality gates before any app code so every later change lands under the same rules: Node 26 via .nvmrc and engines, TypeScript 7 in strict mode with the extra strictness flags, swc for transpiling, oxlint with type-aware, Unicorn and security rules (eslint-plugin-security and no-unsanitized loaded as JS plugins), oxfmt, Vitest with a 100% coverage gate, and husky hooks that run the checks and commitlint. Type escape hatches, lint suppressions and coverage exemptions are banned unless registered in exceptions.json. scripts/audit-exceptions enforces that, and its test runs it against this repository, so an unregistered suppression fails both the pre-commit hook and CI. npm 11 blocks dependency install scripts by default. None are needed: Electron downloads its binary on first use, swc's native binding comes from optionalDependencies, fsevents ships prebuilt, and electron-winstaller only serves Squirrel installers, which aren't used. They are recorded as denied in allowScripts.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import { afterEach, expect, it } from 'vitest';
|
||||
|
||||
afterEach(() => {
|
||||
process.exitCode = undefined;
|
||||
});
|
||||
|
||||
it('passes on this repository', async () => {
|
||||
await import('./audit-exceptions.ts');
|
||||
|
||||
expect(process.exitCode).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runAudit } from './lib/exceptions.ts';
|
||||
|
||||
process.exitCode = await runAudit(process.cwd(), (line) => {
|
||||
console.log(line);
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { audit, parseRegistry, runAudit, unitCoverageExemptPaths } from './exceptions.ts';
|
||||
|
||||
const approved = (id: string, kind: string, paths: string[]): Record<string, unknown> => ({
|
||||
id,
|
||||
kind,
|
||||
paths,
|
||||
reason: 'test reason',
|
||||
approvedBy: 'test approver',
|
||||
});
|
||||
|
||||
describe('parseRegistry', () => {
|
||||
it('accepts a well-formed registry', () => {
|
||||
const registry = parseRegistry({
|
||||
exceptions: [approved('LINT-1', 'lint-suppression', ['src/a.ts'])],
|
||||
});
|
||||
|
||||
expect(registry.exceptions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('rejects entries with an unknown kind', () => {
|
||||
expect(() =>
|
||||
parseRegistry({ exceptions: [approved('X-1', 'anything-goes', ['src/a.ts'])] }),
|
||||
).toThrow(/kind/u);
|
||||
});
|
||||
|
||||
it('rejects duplicate ids', () => {
|
||||
expect(() =>
|
||||
parseRegistry({
|
||||
exceptions: [
|
||||
approved('LINT-1', 'lint-suppression', ['src/a.ts']),
|
||||
approved('LINT-1', 'lint-suppression', ['src/b.ts']),
|
||||
],
|
||||
}),
|
||||
).toThrow(/duplicate id LINT-1/u);
|
||||
});
|
||||
|
||||
it('rejects ids that the APPROVED(...) marker cannot express', () => {
|
||||
expect(() =>
|
||||
parseRegistry({ exceptions: [approved('lint one', 'lint-suppression', ['src/a.ts'])] }),
|
||||
).toThrow(/id/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe('audit', () => {
|
||||
const registry = parseRegistry({
|
||||
exceptions: [
|
||||
approved('LINT-1', 'lint-suppression', ['src/a.ts']),
|
||||
approved('COV-1', 'coverage-ignore', ['src/**/*.ts']),
|
||||
approved('DTS-1', 'dts-shim', ['src/types/mdts.d.ts']),
|
||||
approved('UNIT-1', 'unit-coverage', ['src/main/electron/**']),
|
||||
],
|
||||
});
|
||||
const baseline = [
|
||||
{ path: 'src/a.ts', content: 'export const a = 1;\n' },
|
||||
{ path: 'src/types/mdts.d.ts', content: 'export {};\n' },
|
||||
{ path: 'src/main/electron/menu.ts', content: 'export {};\n' },
|
||||
];
|
||||
|
||||
it('passes a tree with no suppressions and no stale entries', () => {
|
||||
const files = [
|
||||
...baseline,
|
||||
{ path: 'src/a.ts', content: 'x(); // oxlint-disable-line no-x -- APPROVED(LINT-1)\n' },
|
||||
{ path: 'src/b.ts', content: '/* v8 ignore next -- APPROVED(COV-1) */\n' },
|
||||
];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('flags an oxlint suppression without an approval marker', () => {
|
||||
const files = [
|
||||
...baseline,
|
||||
{ path: 'src/a.ts', content: 'const a = 1;\n// oxlint-disable-next-line no-x\n' },
|
||||
];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([
|
||||
'src/a.ts:2: suppression has no APPROVED(<id>) marker',
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats eslint directives, c8 and istanbul comments the same way', () => {
|
||||
const files = [
|
||||
...baseline,
|
||||
{
|
||||
path: 'src/c.ts',
|
||||
content: '/* eslint-disable no-x */\n/* c8 ignore next */\n// istanbul ignore else\n',
|
||||
},
|
||||
];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([
|
||||
'src/c.ts:1: suppression has no APPROVED(<id>) marker',
|
||||
'src/c.ts:2: suppression has no APPROVED(<id>) marker',
|
||||
'src/c.ts:3: suppression has no APPROVED(<id>) marker',
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores directive text inside string literals', () => {
|
||||
const content = [
|
||||
"const a = '// oxlint-disable';",
|
||||
"const b = 'x(); /* v8 ignore next */';",
|
||||
'const c = "it\\"s // eslint-disable";',
|
||||
'const d = `// c8 ignore next`;',
|
||||
"// a comment that doesn't disable anything",
|
||||
'',
|
||||
].join('\n');
|
||||
const files = [...baseline, { path: 'src/d.ts', content }];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('flags an approval id that is not in the registry', () => {
|
||||
const files = [
|
||||
...baseline,
|
||||
{ path: 'src/a.ts', content: '// oxlint-disable -- APPROVED(NOPE)\n' },
|
||||
];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([
|
||||
'src/a.ts:1: APPROVED(NOPE) is not in exceptions.json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('flags an approval whose entry does not cover the file', () => {
|
||||
const files = [
|
||||
...baseline,
|
||||
{ path: 'src/other.ts', content: '// oxlint-disable-line -- APPROVED(LINT-1)\n' },
|
||||
];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([
|
||||
'src/other.ts:1: APPROVED(LINT-1) does not cover this file',
|
||||
]);
|
||||
});
|
||||
|
||||
it('flags an approval of the wrong kind', () => {
|
||||
const files = [
|
||||
...baseline,
|
||||
{ path: 'src/a.ts', content: '// oxlint-disable-line -- APPROVED(COV-1)\n' },
|
||||
];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([
|
||||
'src/a.ts:1: APPROVED(COV-1) is a coverage-ignore exception, not lint-suppression',
|
||||
]);
|
||||
});
|
||||
|
||||
it('flags a declaration file with no dts-shim entry', () => {
|
||||
const files = [...baseline, { path: 'src/types/other.d.ts', content: 'export {};\n' }];
|
||||
|
||||
expect(audit(files, registry)).toStrictEqual([
|
||||
'src/types/other.d.ts: declaration file is not approved in exceptions.json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('flags registry entries that no longer match any file', () => {
|
||||
expect(audit([], registry)).toStrictEqual([
|
||||
'exceptions.json: LINT-1 matches no files',
|
||||
'exceptions.json: COV-1 matches no files',
|
||||
'exceptions.json: DTS-1 matches no files',
|
||||
'exceptions.json: UNIT-1 matches no files',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unitCoverageExemptPaths', () => {
|
||||
it('lists the paths of unit-coverage entries only', () => {
|
||||
const registry = parseRegistry({
|
||||
exceptions: [
|
||||
approved('UNIT-1', 'unit-coverage', ['src/main/electron/**', 'src/main/index.ts']),
|
||||
approved('LINT-1', 'lint-suppression', ['src/a.ts']),
|
||||
],
|
||||
});
|
||||
|
||||
expect(unitCoverageExemptPaths(registry)).toStrictEqual([
|
||||
'src/main/electron/**',
|
||||
'src/main/index.ts',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runAudit', () => {
|
||||
let root = '';
|
||||
const lines: string[] = [];
|
||||
const log = (line: string): void => {
|
||||
lines.push(line);
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(tmpdir(), 'audit-'));
|
||||
lines.length = 0;
|
||||
await mkdir(path.join(root, 'src'), { recursive: true });
|
||||
await mkdir(path.join(root, 'node_modules', 'pkg'), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(root, 'exceptions.json'),
|
||||
JSON.stringify({ exceptions: [approved('LINT-1', 'lint-suppression', ['src/a.ts'])] }),
|
||||
);
|
||||
await writeFile(path.join(root, 'node_modules', 'pkg', 'index.d.ts'), 'export {};\n');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns 0 and reports success for a clean tree', async () => {
|
||||
await writeFile(path.join(root, 'src', 'a.ts'), '// oxlint-disable-line -- APPROVED(LINT-1)\n');
|
||||
|
||||
await expect(runAudit(root, log)).resolves.toBe(0);
|
||||
expect(lines).toStrictEqual(['audit-exceptions: 1 approved exception, no problems']);
|
||||
});
|
||||
|
||||
it('returns 1 and prints every problem', async () => {
|
||||
await writeFile(path.join(root, 'src', 'a.ts'), '// oxlint-disable-line no-x\n');
|
||||
|
||||
await expect(runAudit(root, log)).resolves.toBe(1);
|
||||
expect(lines).toStrictEqual(['src/a.ts:1: suppression has no APPROVED(<id>) marker']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { globSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const kinds = ['lint-suppression', 'coverage-ignore', 'dts-shim', 'unit-coverage'] as const;
|
||||
type Kind = (typeof kinds)[number];
|
||||
|
||||
const entrySchema = z.strictObject({
|
||||
id: z.string().regex(/^[A-Z0-9-]+$/u, 'id must be upper-case letters, digits and dashes'),
|
||||
kind: z.enum(kinds),
|
||||
paths: z.array(z.string().min(1)).min(1),
|
||||
reason: z.string().min(1),
|
||||
approvedBy: z.string().min(1),
|
||||
});
|
||||
|
||||
const registrySchema = z
|
||||
.object({ exceptions: z.array(entrySchema) })
|
||||
.superRefine((registry, context) => {
|
||||
const seen = new Set<string>();
|
||||
for (const entry of registry.exceptions) {
|
||||
if (seen.has(entry.id)) {
|
||||
context.addIssue({ code: 'custom', message: `duplicate id ${entry.id}` });
|
||||
}
|
||||
seen.add(entry.id);
|
||||
}
|
||||
});
|
||||
|
||||
export type Registry = z.infer<typeof registrySchema>;
|
||||
type Entry = Registry['exceptions'][number];
|
||||
|
||||
export interface SourceFile {
|
||||
readonly path: string;
|
||||
readonly content: string;
|
||||
}
|
||||
|
||||
export const parseRegistry = (value: unknown): Registry => registrySchema.parse(value);
|
||||
|
||||
export const unitCoverageExemptPaths = (registry: Registry): string[] =>
|
||||
registry.exceptions
|
||||
.filter((entry) => entry.kind === 'unit-coverage')
|
||||
.flatMap((entry) => entry.paths);
|
||||
|
||||
// Directive text inside string literals (test fixtures, messages) isn't a
|
||||
// directive, so literals are blanked out before matching.
|
||||
const stringLiteral = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/gu;
|
||||
const lintDirective = /(?:\/\/|\/\*)\s*(?:oxlint|eslint)-disable/u;
|
||||
const coverageDirective = /(?:\/\/|\/\*)\s*(?:v8|c8|istanbul) ignore/u;
|
||||
const approvalMarker = /APPROVED\(([^)]*)\)/u;
|
||||
|
||||
const matches = (entry: Entry, file: string): boolean =>
|
||||
entry.paths.some((pattern) => path.posix.matchesGlob(file, pattern));
|
||||
|
||||
const directiveKind = (line: string): Kind | undefined => {
|
||||
const code = line.replaceAll(stringLiteral, "''");
|
||||
if (lintDirective.test(code)) {
|
||||
return 'lint-suppression';
|
||||
}
|
||||
if (coverageDirective.test(code)) {
|
||||
return 'coverage-ignore';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const checkLine = (
|
||||
file: string,
|
||||
lineNumber: number,
|
||||
line: string,
|
||||
byId: ReadonlyMap<string, Entry>,
|
||||
): string | undefined => {
|
||||
const kind = directiveKind(line);
|
||||
if (kind === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const where = `${file}:${String(lineNumber)}`;
|
||||
const id = approvalMarker.exec(line)?.[1];
|
||||
if (id === undefined) {
|
||||
return `${where}: suppression has no APPROVED(<id>) marker`;
|
||||
}
|
||||
const entry = byId.get(id);
|
||||
if (entry === undefined) {
|
||||
return `${where}: APPROVED(${id}) is not in exceptions.json`;
|
||||
}
|
||||
if (entry.kind !== kind) {
|
||||
return `${where}: APPROVED(${id}) is a ${entry.kind} exception, not ${kind}`;
|
||||
}
|
||||
if (!matches(entry, file)) {
|
||||
return `${where}: APPROVED(${id}) does not cover this file`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const audit = (files: readonly SourceFile[], registry: Registry): string[] => {
|
||||
const byId = new Map(registry.exceptions.map((entry) => [entry.id, entry]));
|
||||
const shims = registry.exceptions.filter((entry) => entry.kind === 'dts-shim');
|
||||
const problems: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
for (const [index, line] of file.content.split('\n').entries()) {
|
||||
const problem = checkLine(file.path, index + 1, line, byId);
|
||||
if (problem !== undefined) {
|
||||
problems.push(problem);
|
||||
}
|
||||
}
|
||||
if (file.path.endsWith('.d.ts') && !shims.some((entry) => matches(entry, file.path))) {
|
||||
problems.push(`${file.path}: declaration file is not approved in exceptions.json`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of registry.exceptions) {
|
||||
if (!files.some((file) => matches(entry, file.path))) {
|
||||
problems.push(`exceptions.json: ${entry.id} matches no files`);
|
||||
}
|
||||
}
|
||||
|
||||
return problems;
|
||||
};
|
||||
|
||||
const sourceGlobs = ['**/*.{ts,mts,cts,js,mjs,cjs}'];
|
||||
const ignoredGlobs = [
|
||||
'**/node_modules/**',
|
||||
'dist/**',
|
||||
'release/**',
|
||||
'coverage/**',
|
||||
'e2e/docs-fixture/**',
|
||||
];
|
||||
|
||||
export const runAudit = async (root: string, log: (line: string) => void): Promise<number> => {
|
||||
const registry = parseRegistry(
|
||||
JSON.parse(await readFile(path.join(root, 'exceptions.json'), 'utf8')),
|
||||
);
|
||||
const relativePaths = globSync(sourceGlobs, { cwd: root, exclude: ignoredGlobs });
|
||||
const files = await Promise.all(
|
||||
relativePaths.map(async (relativePath) => ({
|
||||
path: relativePath.split(path.sep).join('/'),
|
||||
content: await readFile(path.join(root, relativePath), 'utf8'),
|
||||
})),
|
||||
);
|
||||
|
||||
const problems = audit(files, registry);
|
||||
for (const problem of problems) {
|
||||
log(problem);
|
||||
}
|
||||
if (problems.length > 0) {
|
||||
return 1;
|
||||
}
|
||||
const count = registry.exceptions.length;
|
||||
log(
|
||||
`audit-exceptions: ${String(count)} approved exception${count === 1 ? '' : 's'}, no problems`,
|
||||
);
|
||||
return 0;
|
||||
};
|
||||
Reference in New Issue
Block a user