@davidvornholt/standards 0.29.0 → 0.29.2

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 CHANGED
@@ -97,14 +97,14 @@ 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, root and 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
101
 
102
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
103
 
104
- 1. Install the new CLI with `bun add --dev --exact @davidvornholt/standards@0.29.0`.
104
+ 1. Install the new CLI with `bun add --dev --exact @davidvornholt/standards@0.29.2`.
105
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
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`.
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
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.
109
109
 
110
110
  ## dependabot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.29.0",
3
+ "version": "0.29.2",
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",
package/src/cli.ts CHANGED
@@ -1326,9 +1326,9 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
1326
1326
  };
1327
1327
  };
1328
1328
 
1329
- // Canonical monorepo structure gate: workspace and root script shapes,
1330
- // internal versioning, `exports`, tsconfig inheritance, a11y wiring,
1331
- // 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
1332
1332
  // (SOPS shape mirroring plus the keys the synced workflows require).
1333
1333
  // The `source` profile instead pins the standards template repository's own
1334
1334
  // deliberate exceptions to the consumer contract.
@@ -42,9 +42,7 @@ export const resolveGithubAppName = (
42
42
  // workflow actually uses today; widening it is a deliberate edit here plus a
43
43
  // settings-page change on the existing App, not a default to grow into.
44
44
  const DEFAULT_PERMISSIONS = {
45
- actions: 'read',
46
45
  contents: 'write',
47
- issues: 'write',
48
46
  metadata: 'read',
49
47
  pull_requests: 'write',
50
48
  workflows: 'write',
@@ -13,6 +13,7 @@ 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(
@@ -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] = await Promise.all([
159
- readJsonFile(join(consumer, 'package.json')),
160
- collectCiSecretsProblems(consumer),
161
- collectSopsActionCallerProblems(consumer),
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,
@@ -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
+ };