mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
ci(client): lint:format keeps new client code Prettier-formatted
client/package.json has had a format:check script, but no workflow runs it, and 2144 of the 2171 .ts, .tsx and .css files under src/ and tests/ differ from what .prettierrc asks for. Without a formatter gate the line count in lint:size could also be gamed, and every new file picked its own layout. Formatting everything in one commit would bury the history, so lint:format lists today's unformatted files in scripts/format-baseline.json and only lets the list shrink: a file outside it, any new file in particular, must be formatted, and a listed file that has been formatted must leave the list (--update takes it off and never adds one). A stale entry fails the check, since it would let the next file at that path in unformatted. The check runs Prettier through its API with the config that applies to each file and reads the text with LF line ends, so a Windows checkout agrees with CI about endOfLine. A file Prettier cannot parse counts as unformatted, and a missing config stops the run.
This commit is contained in:
@@ -12,6 +12,7 @@ npm run lint # eslint . (lint:check in CI)
|
||||
npm run lint:pages # enforce the Page pattern (CI gate)
|
||||
npm run lint:rtl # physical left/right sides and rtl-lint-disable comments may only shrink per file (CI gate; --list shows them, --update lowers both baselines)
|
||||
npm run lint:size # no source file or stylesheet past 1000 lines, no test past 2000, counted wrapped at 120 columns; longer ones may only shrink (CI gate; --update lowers the baseline)
|
||||
npm run lint:format # Prettier: every file outside scripts/format-baseline.json must be formatted, the list only shrinks (CI gate; npx prettier --write <file>, then --update)
|
||||
npm run test # vitest run (tests/** + co-located src/**/*.test.{ts,tsx}); also test:unit / test:integration / test:coverage
|
||||
npm run e2e # Playwright (CI runs --project=public --project=app; e2e:report opens the last report)
|
||||
npm run shots # Playwright screenshot project (shots:promote to accept)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"lint:pages": "node scripts/check-page-pattern.mjs",
|
||||
"lint:rtl": "node scripts/rtl-lint.mjs",
|
||||
"lint:size": "node scripts/size-lint.mjs",
|
||||
"lint:format": "node scripts/format-lint.mjs",
|
||||
"check:gl-split": "node scripts/check-gl-split.mjs",
|
||||
"theme:lint": "node scripts/theme-lint.mjs",
|
||||
"theme:lint:strict": "node scripts/theme-lint.mjs --strict",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* lint:format: new code is formatted with Prettier.
|
||||
*
|
||||
* Client code was never Prettier-checked, and almost every file differs from
|
||||
* what .prettierrc asks for. Formatting all of it at once would bury the
|
||||
* history under one commit, so the files that were unformatted when this
|
||||
* check came in are listed in scripts/format-baseline.json and the list only
|
||||
* shrinks: every file outside it, a new one in particular, must be formatted,
|
||||
* and a listed file leaves the list once it has been formatted. The check
|
||||
* covers .ts, .tsx and .css under src/ and tests/.
|
||||
*
|
||||
* npm run lint:format check against the baseline (CI)
|
||||
* npm run lint:format -- --list print every unformatted file
|
||||
* npm run lint:format -- --update take formatted and deleted files off the list;
|
||||
* it never adds one
|
||||
*
|
||||
* To format a file: npx prettier --write <file>. The comparison lives in
|
||||
* scripts/lib/format.mjs.
|
||||
*/
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { check } from './lib/format.mjs'
|
||||
import { runCli } from './lib/ratchet.mjs'
|
||||
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
await runCli('lint:format', (args) => check({ root, update: args.includes('--update'), list: args.includes('--list') }))
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* The comparison behind lint:format (scripts/format-lint.mjs): which files
|
||||
* Prettier would rewrite, against the list of files that were unformatted
|
||||
* when the check came in.
|
||||
*/
|
||||
import { join } from 'node:path'
|
||||
import * as prettier from 'prettier'
|
||||
import { fileList, listFiles, lowerList, RatchetError, readBaseline, readText, toKey, writeBaseline } from './ratchet.mjs'
|
||||
|
||||
export const DIRS = ['src', 'tests']
|
||||
|
||||
export const accepts = (key) => /\.(?:tsx?|css)$/.test(key)
|
||||
|
||||
/**
|
||||
* Whether Prettier leaves the file as it is, with the config that applies to
|
||||
* it. The text is read with LF line ends, so a Windows checkout agrees with CI
|
||||
* about endOfLine. A file Prettier cannot parse is not formatted.
|
||||
*/
|
||||
export async function isFormatted(path) {
|
||||
const options = await prettier.resolveConfig(path)
|
||||
if (!options) throw new RatchetError(`no Prettier config applies to ${path}`)
|
||||
try {
|
||||
return await prettier.check(readText(path), { ...options, filepath: path })
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** The files under root that Prettier would rewrite, keyed by their path from root. */
|
||||
export async function unformatted(root) {
|
||||
const found = []
|
||||
for (const path of listFiles(root, DIRS, accepts)) if (!(await isFormatted(path))) found.push(toKey(root, path))
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the check against the list at baselinePath. A file outside the list
|
||||
* must be formatted. A listed file that is formatted now, or gone, fails the
|
||||
* check as well until --update takes it out: the list only shrinks, and a
|
||||
* stale entry would let the next file at that path in unformatted. Returns
|
||||
* the exit code.
|
||||
*/
|
||||
export async function check({
|
||||
root,
|
||||
baselinePath = join(root, 'scripts/format-baseline.json'),
|
||||
update = false,
|
||||
list = false,
|
||||
log = console.log,
|
||||
error = console.error,
|
||||
}) {
|
||||
const found = await unformatted(root)
|
||||
let baseline = readBaseline(baselinePath, fileList)
|
||||
|
||||
if (update) {
|
||||
baseline = lowerList(baseline, found)
|
||||
writeBaseline(baselinePath, baseline)
|
||||
}
|
||||
|
||||
const allowed = new Set(baseline)
|
||||
const fresh = found.filter((key) => !allowed.has(key))
|
||||
const now = new Set(found)
|
||||
const stale = baseline.filter((key) => !now.has(key))
|
||||
|
||||
if (list) for (const key of found) log(`${allowed.has(key) ? 'listed' : 'new '} ${key}`)
|
||||
for (const key of fresh) {
|
||||
error(`FAIL ${key} is not formatted. Run: npx prettier --write ${key}`)
|
||||
}
|
||||
for (const key of stale) {
|
||||
error(`FAIL ${key} is listed in scripts/format-baseline.json but is formatted now or gone.`)
|
||||
}
|
||||
if (stale.length) error('Run npm run lint:format -- --update to take the formatted files off the list.')
|
||||
log(`format: ${found.length} unformatted file(s), ${baseline.length} listed in the baseline`)
|
||||
return fresh.length || stale.length ? 1 : 0
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { check, isFormatted } from '../../../scripts/lib/format.mjs';
|
||||
import { RatchetError } from '../../../scripts/lib/ratchet.mjs';
|
||||
import { ratchetTree, type RatchetTree } from '../../helpers/ratchetFixture';
|
||||
|
||||
let tree: RatchetTree;
|
||||
afterEach(() => tree?.remove());
|
||||
|
||||
const BASELINE = 'scripts/format-baseline.json';
|
||||
const CONFIG = { '.prettierrc': JSON.stringify({ semi: true, singleQuote: true, endOfLine: 'lf' }) };
|
||||
const FORMATTED = "const a = 'x';\n";
|
||||
const UNFORMATTED = 'const a = "x"\n';
|
||||
|
||||
function run(baseline: string[], files: Record<string, string>, update = false) {
|
||||
tree = ratchetTree({ ...CONFIG, [BASELINE]: JSON.stringify(baseline), 'tests/keep.ts': FORMATTED, ...files });
|
||||
return check({ root: tree.root, baselinePath: tree.path(BASELINE), update, ...tree.out });
|
||||
}
|
||||
|
||||
describe('lint:format', () => {
|
||||
it('FORMAT-001: tells formatted from unformatted files, and reads CRLF as LF', async () => {
|
||||
tree = ratchetTree({
|
||||
...CONFIG,
|
||||
'a.ts': FORMATTED,
|
||||
'b.ts': UNFORMATTED,
|
||||
'c.ts': "const a = 'x';\r\n",
|
||||
'd.ts': 'const (',
|
||||
});
|
||||
expect(await isFormatted(tree.path('a.ts'))).toBe(true);
|
||||
expect(await isFormatted(tree.path('b.ts'))).toBe(false);
|
||||
expect(await isFormatted(tree.path('c.ts'))).toBe(true);
|
||||
expect(await isFormatted(tree.path('d.ts'))).toBe(false);
|
||||
});
|
||||
|
||||
it('FORMAT-002: passes when every unformatted file is listed', async () => {
|
||||
expect(await run(['src/old.ts'], { 'src/old.ts': UNFORMATTED, 'src/new.ts': FORMATTED })).toBe(0);
|
||||
expect(tree.error).toEqual([]);
|
||||
});
|
||||
|
||||
it('FORMAT-003: fails a new file that is not formatted', async () => {
|
||||
expect(await run([], { 'src/new.tsx': UNFORMATTED })).toBe(1);
|
||||
expect(tree.error.join('\n')).toMatch(/src\/new\.tsx is not formatted\. Run: npx prettier --write src\/new\.tsx/);
|
||||
});
|
||||
|
||||
it('FORMAT-004: fails a listed file that is formatted now, until --update takes it off', async () => {
|
||||
expect(await run(['src/done.ts', 'src/gone.ts'], { 'src/done.ts': FORMATTED })).toBe(1);
|
||||
expect(tree.error.join('\n')).toMatch(/src\/done\.ts is listed .* formatted now or gone/);
|
||||
expect(tree.error.join('\n')).toMatch(/src\/gone\.ts is listed/);
|
||||
tree.remove();
|
||||
expect(
|
||||
await run(['src/done.ts', 'src/old.ts'], { 'src/done.ts': FORMATTED, 'src/old.ts': UNFORMATTED }, true)
|
||||
).toBe(0);
|
||||
expect(JSON.parse(readFileSync(tree.path(BASELINE), 'utf8'))).toEqual(['src/old.ts']);
|
||||
});
|
||||
|
||||
it('FORMAT-005: --update never puts a file on the list', async () => {
|
||||
expect(await run([], { 'src/new.ts': UNFORMATTED }, true)).toBe(1);
|
||||
expect(JSON.parse(readFileSync(tree.path(BASELINE), 'utf8'))).toEqual([]);
|
||||
});
|
||||
|
||||
it('FORMAT-006: a missing baseline or Prettier config stops the check', async () => {
|
||||
tree = ratchetTree({ ...CONFIG, 'src/a.ts': FORMATTED, 'tests/a.ts': FORMATTED });
|
||||
await expect(check({ root: tree.root, baselinePath: tree.path(BASELINE), ...tree.out })).rejects.toThrow(
|
||||
RatchetError
|
||||
);
|
||||
tree.remove();
|
||||
tree = ratchetTree({ [BASELINE]: '[]', 'src/a.ts': FORMATTED, 'tests/a.ts': FORMATTED });
|
||||
// No config in the tree; the lookup climbs no further than the temp directory's parents,
|
||||
// which hold none either.
|
||||
const result = check({ root: tree.root, baselinePath: tree.path(BASELINE), ...tree.out });
|
||||
await expect(result).rejects.toThrow(/no Prettier config/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user