@davidvornholt/standards 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/package.json +2 -3
- package/src/cli.ts +31 -7
- package/src/poller-config.ts +0 -2
- package/src/poller-github-pulls.ts +0 -1
- package/src/poller-outcome.ts +6 -2
- package/src/poller-review-plan-parse.ts +4 -2
- package/src/poller-review-thread-write.ts +0 -1
- package/src/structure-check.ts +11 -6
- package/src/structure-profile.ts +11 -0
- package/src/structure-turbo.ts +89 -0
- package/src/structure-workspace.ts +15 -4
package/README.md
CHANGED
|
@@ -97,7 +97,15 @@ bun standards structure --profile source
|
|
|
97
97
|
|
|
98
98
|
`doctor` checks project-owned integration points such as `biome.jsonc`, `AGENTS.local.md`, package scripts, sync policy, settings overlays, and Dependabot inputs.
|
|
99
99
|
|
|
100
|
-
`structure` checks the exact Bun declaration, workspace scripts, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
|
|
100
|
+
`structure` checks the exact Bun declaration, root and workspace scripts, the root lint tasks in `turbo.json`, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
|
|
101
|
+
|
|
102
|
+
Starting with 0.29.0, Biome runs once from the repository root instead of once per workspace. The root `package.json` must declare `lint` running `biome check --error-on-warnings .` and `lint:fix` running `biome check --write --error-on-warnings .`, and any workspace that declares `lint` or `lint:fix` fails. To upgrade an existing repository:
|
|
103
|
+
|
|
104
|
+
1. Install the new CLI with `bun add --dev --exact @davidvornholt/standards@0.29.1`.
|
|
105
|
+
2. Run `bun standards sync` so the canonical `packages/typescript-config` and `packages/a11y-testing` drop their lint scripts. A repository that pins `ref` in `sync-standards.local.json` first moves it to `v0.29.0` or later.
|
|
106
|
+
3. Add the root `lint` and `lint:fix` scripts and remove both scripts from every workspace `package.json`.
|
|
107
|
+
4. In `turbo.json`, replace the `lint` and `lint:fix` tasks with `//#lint` (`"outputs": []`) and `//#lint:fix` (`"cache": false`, `"outputs": []`), and delete any `<package>#lint` overrides. Keep the default inputs for `//#lint`. Starting with 0.29.1, `structure` fails until this step is done: Turbo runs the root lint scripts only through the `//#` tasks, so a leftover `lint` task makes `turbo run lint` pass without running Biome.
|
|
108
|
+
5. Run `bun run lint:fix` and fix anything it still reports. The root run also checks root files that per-workspace runs never covered.
|
|
101
109
|
|
|
102
110
|
## dependabot
|
|
103
111
|
|
|
@@ -240,7 +248,7 @@ Bootstrap credentials and singular tokens still owned by an active repository ar
|
|
|
240
248
|
|
|
241
249
|
### Prepare existing consumers for GitHub App client IDs
|
|
242
250
|
|
|
243
|
-
`standards creds add github --dest ci:ci.broker_app` writes and verifies `app_id`, `client_id`, and `private_key` together.
|
|
251
|
+
`standards creds add github --dest ci:ci.broker_app` writes and verifies `app_id`, `client_id`, and `private_key` together. The canonical Standards sync workflow passes `client-id` to `actions/create-github-app-token@v3` when `ci.broker_app.client_id` exists and falls back to the deprecated `app-id` when it does not. Provision `client_id` in each existing consumer before a future release drops that fallback: re-run `bun standards creds add github --dest ci:ci.broker_app`, or add the App's public client ID at `ci.broker_app.client_id` with `sops`. Mirror the new key with a placeholder in `secrets/ci.example.yaml` and commit both files. Keep the numeric `app_id` until every local and canonical caller has switched to `client-id`.
|
|
244
252
|
|
|
245
253
|
## screenshots
|
|
246
254
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidvornholt/standards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -173,6 +173,7 @@
|
|
|
173
173
|
"src/structure-script.ts",
|
|
174
174
|
"src/structure-secrets.ts",
|
|
175
175
|
"src/structure-sops-envelope.ts",
|
|
176
|
+
"src/structure-turbo.ts",
|
|
176
177
|
"src/structure-workspace.ts",
|
|
177
178
|
"src/sync-policy.ts",
|
|
178
179
|
"src/yaml-emit.ts",
|
|
@@ -180,8 +181,6 @@
|
|
|
180
181
|
"src/sops-action-callers.ts"
|
|
181
182
|
],
|
|
182
183
|
"scripts": {
|
|
183
|
-
"lint": "biome check --error-on-warnings .",
|
|
184
|
-
"lint:fix": "biome check --write --error-on-warnings .",
|
|
185
184
|
"check-types": "tsc --noEmit",
|
|
186
185
|
"test": "bun test"
|
|
187
186
|
},
|
package/src/cli.ts
CHANGED
|
@@ -389,11 +389,31 @@ type MirrorResult = {
|
|
|
389
389
|
readonly stale: ReadonlyArray<string>;
|
|
390
390
|
};
|
|
391
391
|
|
|
392
|
+
type MirrorCommand = 'init' | 'sync';
|
|
393
|
+
|
|
392
394
|
type MirrorOptions = {
|
|
393
395
|
readonly upstream: ReadonlyMap<string, ManagedEntry>;
|
|
394
396
|
readonly consumer: string;
|
|
395
397
|
readonly previous: Record<string, string>;
|
|
396
398
|
readonly dryRun: boolean;
|
|
399
|
+
readonly command: MirrorCommand;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// A copy or zip tool that follows links turns a managed link into a directory of
|
|
403
|
+
// copies. Those copies are not in the lock, so the run refuses; for a link the
|
|
404
|
+
// message names what the path should be and the exact way back to it.
|
|
405
|
+
const directoryCollision = (
|
|
406
|
+
rel: string,
|
|
407
|
+
entry: ManagedEntry,
|
|
408
|
+
unlocked: ReadonlyArray<string>,
|
|
409
|
+
command: MirrorCommand,
|
|
410
|
+
): string => {
|
|
411
|
+
const holding = `${unlocked.length} path(s) this repository does not manage (${unlocked.slice(0, COLLISION_SAMPLE).join(', ')})`;
|
|
412
|
+
if (entry.kind !== 'symlink') {
|
|
413
|
+
return `${rel} is a directory holding ${holding}; move or delete them, then re-run`;
|
|
414
|
+
}
|
|
415
|
+
const canonical = posix.join(posix.dirname(rel), entry.target);
|
|
416
|
+
return `${rel} should be a symlink to the canonical ${canonical}, but it is a directory holding ${holding}; copy and zip tools that follow links can cause this. First move any of your own files out of ${rel}, then run \`rm -rf ${rel} && bun standards ${command}\``;
|
|
397
417
|
};
|
|
398
418
|
|
|
399
419
|
// Two things init/sync must settle before their first write: an escaping
|
|
@@ -408,10 +428,11 @@ const mirrorPreconditionProblems = async (
|
|
|
408
428
|
consumer: string,
|
|
409
429
|
upstream: ReadonlyMap<string, ManagedEntry>,
|
|
410
430
|
previous: Record<string, string>,
|
|
431
|
+
command: MirrorCommand,
|
|
411
432
|
): Promise<ReadonlyArray<string>> => {
|
|
412
433
|
const locked = new Set(Object.keys(previous));
|
|
413
434
|
const collisions = await Promise.all(
|
|
414
|
-
[...upstream
|
|
435
|
+
[...upstream].map(async ([rel, entry]) => {
|
|
415
436
|
const ancestor = await interposedAncestor(consumer, rel, upstream);
|
|
416
437
|
if (ancestor !== null) {
|
|
417
438
|
return `${rel} is below the ${ancestor.kind} ${ancestor.rel}; refusing to write through an existing file or symlink ancestor; move it aside before syncing this payload`;
|
|
@@ -423,7 +444,7 @@ const mirrorPreconditionProblems = async (
|
|
|
423
444
|
const unlocked = await unlockedPathsUnder(consumer, rel, locked);
|
|
424
445
|
return unlocked.length === 0
|
|
425
446
|
? null
|
|
426
|
-
:
|
|
447
|
+
: directoryCollision(rel, entry, unlocked, command);
|
|
427
448
|
}),
|
|
428
449
|
);
|
|
429
450
|
return [
|
|
@@ -545,12 +566,13 @@ const mirror = async ({
|
|
|
545
566
|
consumer,
|
|
546
567
|
previous,
|
|
547
568
|
dryRun,
|
|
569
|
+
command,
|
|
548
570
|
}: MirrorOptions): Promise<MirrorResult> => {
|
|
549
571
|
for (const rel of Object.keys(previous)) {
|
|
550
572
|
assertSafeRelativePath(rel, 'sync-standards.lock file');
|
|
551
573
|
}
|
|
552
574
|
assertNoProblems(
|
|
553
|
-
await mirrorPreconditionProblems(consumer, upstream, previous),
|
|
575
|
+
await mirrorPreconditionProblems(consumer, upstream, previous, command),
|
|
554
576
|
'cannot mirror canonical files',
|
|
555
577
|
);
|
|
556
578
|
const next: Record<string, string> = {};
|
|
@@ -682,7 +704,7 @@ const runInit = async (
|
|
|
682
704
|
assertNoProblems(
|
|
683
705
|
[
|
|
684
706
|
...symlinkTargetProblems(seeds),
|
|
685
|
-
...(await mirrorPreconditionProblems(consumer, upstream, {})),
|
|
707
|
+
...(await mirrorPreconditionProblems(consumer, upstream, {}, 'init')),
|
|
686
708
|
],
|
|
687
709
|
'cannot initialize from this source',
|
|
688
710
|
);
|
|
@@ -712,6 +734,7 @@ const runInit = async (
|
|
|
712
734
|
consumer,
|
|
713
735
|
previous: {},
|
|
714
736
|
dryRun: false,
|
|
737
|
+
command: 'init',
|
|
715
738
|
});
|
|
716
739
|
reportMirror(result, false);
|
|
717
740
|
await applyProspectiveDependabot(consumer, prospectiveDependabot, false);
|
|
@@ -745,6 +768,7 @@ const runSync = async (
|
|
|
745
768
|
consumer,
|
|
746
769
|
previous: lock?.files ?? {},
|
|
747
770
|
dryRun,
|
|
771
|
+
command: 'sync',
|
|
748
772
|
});
|
|
749
773
|
reportMirror(result, dryRun);
|
|
750
774
|
const generated = await applyProspectiveDependabot(
|
|
@@ -1302,9 +1326,9 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
1302
1326
|
};
|
|
1303
1327
|
};
|
|
1304
1328
|
|
|
1305
|
-
// Canonical monorepo structure gate: workspace and root script shapes,
|
|
1306
|
-
// internal versioning, `exports`, tsconfig
|
|
1307
|
-
// and the undecrypted CI secrets contract
|
|
1329
|
+
// Canonical monorepo structure gate: workspace and root script shapes, the
|
|
1330
|
+
// root lint tasks in turbo.json, internal versioning, `exports`, tsconfig
|
|
1331
|
+
// inheritance, a11y wiring, and the undecrypted CI secrets contract
|
|
1308
1332
|
// (SOPS shape mirroring plus the keys the synced workflows require).
|
|
1309
1333
|
// The `source` profile instead pins the standards template repository's own
|
|
1310
1334
|
// deliberate exceptions to the consumer contract.
|
package/src/poller-config.ts
CHANGED
|
@@ -14,7 +14,6 @@ const CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
|
14
14
|
'repos',
|
|
15
15
|
'model',
|
|
16
16
|
'reasoningEffort',
|
|
17
|
-
// biome-ignore lint/security/noSecrets: a config key name, not a credential.
|
|
18
17
|
'maxJobsPerTick',
|
|
19
18
|
'staleClaimHours',
|
|
20
19
|
'runTimeoutMinutes',
|
|
@@ -167,7 +166,6 @@ export const parsePollerConfig = (
|
|
|
167
166
|
),
|
|
168
167
|
maxJobsPerTick: parsePositiveInteger(
|
|
169
168
|
raw.maxJobsPerTick,
|
|
170
|
-
// biome-ignore lint/security/noSecrets: a config key name, not a credential.
|
|
171
169
|
'maxJobsPerTick',
|
|
172
170
|
DEFAULT_MAX_JOBS_PER_TICK,
|
|
173
171
|
problems,
|
|
@@ -175,7 +175,6 @@ export const markPullRequestReady = async (
|
|
|
175
175
|
): Promise<void> => {
|
|
176
176
|
const response = await request(token, 'POST', '/graphql', {
|
|
177
177
|
query:
|
|
178
|
-
// biome-ignore lint/security/noSecrets: a GraphQL mutation string, not a credential.
|
|
179
178
|
'mutation($id: ID!) { markPullRequestReadyForReview(input: {pullRequestId: $id}) { pullRequest { isDraft } } }',
|
|
180
179
|
variables: { id: pullRequestNodeId },
|
|
181
180
|
});
|
package/src/poller-outcome.ts
CHANGED
|
@@ -19,8 +19,12 @@ const hasExactKeys = (
|
|
|
19
19
|
value: Readonly<Record<string, unknown>>,
|
|
20
20
|
expected: ReadonlyArray<string>,
|
|
21
21
|
): boolean => {
|
|
22
|
-
const actual = Object.keys(value).sort()
|
|
23
|
-
|
|
22
|
+
const actual = Object.keys(value).sort((left, right) =>
|
|
23
|
+
left.localeCompare(right),
|
|
24
|
+
);
|
|
25
|
+
const sortedExpected = [...expected].sort((left, right) =>
|
|
26
|
+
left.localeCompare(right),
|
|
27
|
+
);
|
|
24
28
|
return (
|
|
25
29
|
actual.length === sortedExpected.length &&
|
|
26
30
|
actual.every((key, index) => key === sortedExpected[index])
|
|
@@ -22,8 +22,10 @@ const hasExactKeys = (
|
|
|
22
22
|
value: Readonly<Record<string, unknown>>,
|
|
23
23
|
keys: ReadonlyArray<string>,
|
|
24
24
|
): boolean => {
|
|
25
|
-
const actual = Object.keys(value).sort()
|
|
26
|
-
|
|
25
|
+
const actual = Object.keys(value).sort((left, right) =>
|
|
26
|
+
left.localeCompare(right),
|
|
27
|
+
);
|
|
28
|
+
const expected = [...keys].sort((left, right) => left.localeCompare(right));
|
|
27
29
|
return (
|
|
28
30
|
actual.length === expected.length &&
|
|
29
31
|
actual.every((key, index) => key === expected[index])
|
|
@@ -2,7 +2,6 @@ import { apiError, HTTP_OK, request } from './github-api';
|
|
|
2
2
|
import { isRecord } from './github-settings-parse';
|
|
3
3
|
|
|
4
4
|
const REPLY_MUTATION =
|
|
5
|
-
// biome-ignore lint/security/noSecrets: a GraphQL mutation string, not a credential.
|
|
6
5
|
'mutation($id: ID!, $body: String!) { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $id, body: $body }) { comment { id } } }';
|
|
7
6
|
|
|
8
7
|
const RESOLVE_MUTATION =
|
package/src/structure-check.ts
CHANGED
|
@@ -13,10 +13,11 @@ import {
|
|
|
13
13
|
} from './structure-profile';
|
|
14
14
|
import { filteredTurboAliasProblem, hasSafeCommands } from './structure-script';
|
|
15
15
|
import { collectCiSecretsProblems } from './structure-secrets';
|
|
16
|
+
import { collectTurboLintProblems } from './structure-turbo';
|
|
16
17
|
import { inspectWorkspace, type Workspace } from './structure-workspace';
|
|
17
18
|
|
|
18
19
|
const ROOT_FIXED_SCRIPTS = new Set(
|
|
19
|
-
'standards,check,check:fix,test:a11y'.split(','),
|
|
20
|
+
'standards,check,check:fix,lint,lint:fix,test:a11y'.split(','),
|
|
20
21
|
);
|
|
21
22
|
const GLOB_SUFFIX = '/*';
|
|
22
23
|
const WORKSPACES_REQUIREMENT =
|
|
@@ -155,14 +156,17 @@ export const collectStructureProblems = async (
|
|
|
155
156
|
consumer: string,
|
|
156
157
|
profile: StructureProfile,
|
|
157
158
|
): Promise<ReadonlyArray<string>> => {
|
|
158
|
-
const [root, secretsProblems, callerProblems] =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
const [root, turboProblems, secretsProblems, callerProblems] =
|
|
160
|
+
await Promise.all([
|
|
161
|
+
readJsonFile(join(consumer, 'package.json')),
|
|
162
|
+
collectTurboLintProblems(consumer),
|
|
163
|
+
collectCiSecretsProblems(consumer),
|
|
164
|
+
collectSopsActionCallerProblems(consumer),
|
|
165
|
+
]);
|
|
163
166
|
if (root === null) {
|
|
164
167
|
return [
|
|
165
168
|
'package.json must exist and contain a JSON object',
|
|
169
|
+
...turboProblems,
|
|
166
170
|
...secretsProblems,
|
|
167
171
|
...callerProblems,
|
|
168
172
|
];
|
|
@@ -194,6 +198,7 @@ export const collectStructureProblems = async (
|
|
|
194
198
|
...missingPublishedCliProblems(profile, workspaces),
|
|
195
199
|
...collectBunVersionProblems(root),
|
|
196
200
|
...inspectRootScripts(root, profile, requireA11y),
|
|
201
|
+
...turboProblems,
|
|
197
202
|
...inspections.flatMap((i) => [...i.problems]),
|
|
198
203
|
...secretsProblems,
|
|
199
204
|
...callerProblems,
|
package/src/structure-profile.ts
CHANGED
|
@@ -13,6 +13,8 @@ const CONSUMER_CHECK =
|
|
|
13
13
|
const CONSUMER_CHECK_FIX =
|
|
14
14
|
'turbo run lint:fix check-types test build test:a11y --output-logs=errors-only';
|
|
15
15
|
const ROOT_A11Y = 'turbo run test:a11y';
|
|
16
|
+
const ROOT_LINT = 'biome check --error-on-warnings .';
|
|
17
|
+
const ROOT_LINT_FIX = 'biome check --write --error-on-warnings .';
|
|
16
18
|
|
|
17
19
|
export const PUBLISHED_CLI_WORKSPACE = 'packages/standards-cli';
|
|
18
20
|
const PUBLISHED_CLI_PACKAGE = '@davidvornholt/standards';
|
|
@@ -51,6 +53,14 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
|
|
|
51
53
|
},
|
|
52
54
|
];
|
|
53
55
|
|
|
56
|
+
// Biome runs once over the whole repository from the root. With the project
|
|
57
|
+
// domain on, every invocation loads the entire repository, so one root run
|
|
58
|
+
// replaces a full project load per workspace and also covers root files.
|
|
59
|
+
const ROOT_LINT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
|
|
60
|
+
{ name: 'lint', commands: [ROOT_LINT], exact: true },
|
|
61
|
+
{ name: 'lint:fix', commands: [ROOT_LINT_FIX], exact: true },
|
|
62
|
+
];
|
|
63
|
+
|
|
54
64
|
export const rootScriptExpectations = (
|
|
55
65
|
profile: StructureProfile,
|
|
56
66
|
requireA11y: boolean,
|
|
@@ -61,6 +71,7 @@ export const rootScriptExpectations = (
|
|
|
61
71
|
{ name: 'check', commands: [CONSUMER_CHECK], exact: false },
|
|
62
72
|
{ name: 'check:fix', commands: [CONSUMER_CHECK_FIX], exact: false },
|
|
63
73
|
]),
|
|
74
|
+
...ROOT_LINT_EXPECTATIONS,
|
|
64
75
|
...(requireA11y
|
|
65
76
|
? [{ name: 'test:a11y', commands: [ROOT_A11Y], exact: false }]
|
|
66
77
|
: []),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isRecord } from './github-settings-parse';
|
|
4
|
+
|
|
5
|
+
// The root gate runs `turbo run lint` and `turbo run lint:fix`, and Turbo runs
|
|
6
|
+
// a root package.json script only through a task registered as `//#<name>`.
|
|
7
|
+
// A plain `lint` task targets workspace scripts, which no longer exist, so
|
|
8
|
+
// Turbo runs nothing and the gate passes without linting. A cache hit also
|
|
9
|
+
// skips Biome: narrowed `//#lint` inputs let CI replay main's cached pass after
|
|
10
|
+
// a linted file changes, and a cached `//#lint:fix` replays instead of fixing.
|
|
11
|
+
const ROOT_LINT_TASK = '//#lint';
|
|
12
|
+
const ROOT_LINT_FIX_TASK = '//#lint:fix';
|
|
13
|
+
const LINT_TASK_NAMES: ReadonlyArray<string> = ['lint', 'lint:fix'];
|
|
14
|
+
const ROOT_PACKAGE = '//';
|
|
15
|
+
|
|
16
|
+
const readTurboConfig = async (
|
|
17
|
+
consumer: string,
|
|
18
|
+
): Promise<Record<string, unknown> | null> => {
|
|
19
|
+
const raw = await readFile(join(consumer, 'turbo.json'), 'utf8').catch(
|
|
20
|
+
() => null,
|
|
21
|
+
);
|
|
22
|
+
if (raw === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
// Turbo accepts comments and trailing commas in turbo.json.
|
|
27
|
+
const parsed: unknown = globalThis.Bun.JSONC.parse(raw);
|
|
28
|
+
return isRecord(parsed) ? parsed : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Matches `lint`, `lint:fix`, and `<package>#lint(:fix)` overrides, but not
|
|
35
|
+
// the root `//#` tasks.
|
|
36
|
+
const isWorkspaceLintTask = (key: string): boolean => {
|
|
37
|
+
const separator = key.lastIndexOf('#');
|
|
38
|
+
const owner = separator === -1 ? null : key.slice(0, separator);
|
|
39
|
+
return (
|
|
40
|
+
owner !== ROOT_PACKAGE && LINT_TASK_NAMES.includes(key.slice(separator + 1))
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const rootLintProblems = (
|
|
45
|
+
tasks: Record<string, unknown>,
|
|
46
|
+
): ReadonlyArray<string> => {
|
|
47
|
+
const { [ROOT_LINT_TASK]: lint, [ROOT_LINT_FIX_TASK]: lintFix } = tasks;
|
|
48
|
+
return [
|
|
49
|
+
...(isRecord(lint)
|
|
50
|
+
? []
|
|
51
|
+
: [
|
|
52
|
+
`turbo.json: define the root task "${ROOT_LINT_TASK}" as { "outputs": [] }; Turbo runs the root "lint" script only through that task, so without it turbo run lint never runs Biome`,
|
|
53
|
+
]),
|
|
54
|
+
...(isRecord(lint) && Object.hasOwn(lint, 'inputs')
|
|
55
|
+
? [
|
|
56
|
+
`turbo.json: remove "inputs" from the root task "${ROOT_LINT_TASK}"; the default inputs cover every file Biome checks, and a narrower set replays a cached pass after a linted file changes`,
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
59
|
+
...(isRecord(lintFix)
|
|
60
|
+
? []
|
|
61
|
+
: [
|
|
62
|
+
`turbo.json: define the root task "${ROOT_LINT_FIX_TASK}" as { "cache": false, "outputs": [] }; Turbo runs the root "lint:fix" script only through that task, so without it turbo run lint:fix never runs Biome`,
|
|
63
|
+
]),
|
|
64
|
+
...(isRecord(lintFix) && lintFix.cache !== false
|
|
65
|
+
? [
|
|
66
|
+
`turbo.json: set "cache": false on the root task "${ROOT_LINT_FIX_TASK}"; a cache hit replays the previous output instead of running Biome's fixes`,
|
|
67
|
+
]
|
|
68
|
+
: []),
|
|
69
|
+
];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const collectTurboLintProblems = async (
|
|
73
|
+
consumer: string,
|
|
74
|
+
): Promise<ReadonlyArray<string>> => {
|
|
75
|
+
const config = await readTurboConfig(consumer);
|
|
76
|
+
if (config === null) {
|
|
77
|
+
return ['turbo.json must exist and contain a JSON object'];
|
|
78
|
+
}
|
|
79
|
+
const tasks = isRecord(config.tasks) ? config.tasks : {};
|
|
80
|
+
return [
|
|
81
|
+
...rootLintProblems(tasks),
|
|
82
|
+
...Object.keys(tasks)
|
|
83
|
+
.filter(isWorkspaceLintTask)
|
|
84
|
+
.map(
|
|
85
|
+
(key) =>
|
|
86
|
+
`turbo.json: remove task "${key}"; linting runs once at the repository root through the root tasks "${ROOT_LINT_TASK}" and "${ROOT_LINT_FIX_TASK}"`,
|
|
87
|
+
),
|
|
88
|
+
];
|
|
89
|
+
};
|
|
@@ -13,10 +13,12 @@ export type Workspace = {
|
|
|
13
13
|
};
|
|
14
14
|
const WORKSPACE_SCRIPTS: ReadonlyArray<readonly [string, string]> = [
|
|
15
15
|
['check-types', 'tsc --noEmit'],
|
|
16
|
-
['lint', 'biome check --error-on-warnings .'],
|
|
17
|
-
['lint:fix', 'biome check --write --error-on-warnings .'],
|
|
18
16
|
['test', 'bun test'],
|
|
19
17
|
];
|
|
18
|
+
// Biome lints the whole repository once from the root "lint" scripts, and
|
|
19
|
+
// Turbo runs only that root task. A workspace copy would be a stale second
|
|
20
|
+
// entry point, so declaring one fails instead of being silently ignored.
|
|
21
|
+
const ROOT_ONLY_SCRIPTS = ['lint', 'lint:fix'] as const;
|
|
20
22
|
const A11Y_DEPENDENCIES = ['@axe-core/playwright', '@playwright/test'] as const;
|
|
21
23
|
const DEPENDENCY_FIELDS =
|
|
22
24
|
'dependencies devDependencies peerDependencies optionalDependencies'.split(
|
|
@@ -57,8 +59,9 @@ const containsA11ySuite = async (dir: string): Promise<boolean> => {
|
|
|
57
59
|
);
|
|
58
60
|
return nested.includes(true);
|
|
59
61
|
};
|
|
60
|
-
const inspectScripts = (ws: Workspace): ReadonlyArray<string> =>
|
|
61
|
-
|
|
62
|
+
const inspectScripts = (ws: Workspace): ReadonlyArray<string> => {
|
|
63
|
+
const scripts = isRecord(ws.manifest.scripts) ? ws.manifest.scripts : {};
|
|
64
|
+
const required = WORKSPACE_SCRIPTS.flatMap(([name, command]) => {
|
|
62
65
|
const script = scriptOf(ws.manifest, name);
|
|
63
66
|
const accepted =
|
|
64
67
|
hasSafeCommand(script, command) ||
|
|
@@ -67,6 +70,14 @@ const inspectScripts = (ws: Workspace): ReadonlyArray<string> =>
|
|
|
67
70
|
name === 'test' ? 'bun test or bun test --isolate' : command;
|
|
68
71
|
return accepted ? [] : [`${ws.rel}: script "${name}" must run ${expected}`];
|
|
69
72
|
});
|
|
73
|
+
const rootOnly = ROOT_ONLY_SCRIPTS.filter((name) =>
|
|
74
|
+
Object.hasOwn(scripts, name),
|
|
75
|
+
).map(
|
|
76
|
+
(name) =>
|
|
77
|
+
`${ws.rel}: remove script "${name}"; linting runs once at the repository root through the root "lint" and "lint:fix" scripts`,
|
|
78
|
+
);
|
|
79
|
+
return [...required, ...rootOnly];
|
|
80
|
+
};
|
|
70
81
|
const declaredDependencies = (manifest: Record<string, unknown>) =>
|
|
71
82
|
DEPENDENCY_FIELDS.flatMap((field) => {
|
|
72
83
|
const deps = manifest[field];
|