@davidvornholt/standards 0.7.0 → 0.8.0

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
@@ -16,6 +16,8 @@ standards github --apply
16
16
 
17
17
  `structure` enforces the canonical monorepo structure contract: workspace scripts (`check-types`, `lint`, `lint:fix`, `test`), root gate scripts, filtered Turbo convenience aliases, internal `0.0.0`/`workspace:*` versioning, package `exports`, shared tsconfig inheritance, and browser a11y wiring. Only a workspace containing an explicit `*.a11y.ts` suite must provide a `test:a11y` script and direct `@axe-core/playwright` and `@playwright/test` dependencies. `check` includes `structure`, so these rules gate every consumer PR.
18
18
 
19
+ `structure --profile source` validates the standards source repository itself, which is deliberately not a consumer. The profile pins its intentional exceptions so they cannot drift silently: exact, ordered root gate scripts that run this CLI from the local checkout (`structure --profile source`, `github --check`, and the Turbo gate) instead of a recursive `standards check`, and a non-private published bin-only CLI workspace that carries a stable release SemVer and exactly maps the `standards` bin to `src/cli.ts` without `exports`. Every other consumer rule still applies.
20
+
19
21
  ## Configuration
20
22
 
21
23
  - **`sync-standards.local.json`** (optional) — consumer-owned sync policy at the repo root, validated by `doctor`/`check` and every `init`/`sync` even when an explicit ref or local source makes its `"ref"` irrelevant. All fields optional; a missing file means the defaults. `"ref"` is a non-empty single-line string that pins a tag, branch, or full commit sha to sync from instead of `main` (an explicit `--ref` overrides it; a local-path `--from` source is used as-is and ignores only the validated pin). `"autoSync": false` is read by the standards-sync workflow, not the CLI, and skips the weekly scheduled run. Version 0.7.0 removes the legacy `STANDARDS_AUTO_SYNC` and `STANDARDS_SYNC_REF` variable behavior; consumers must upgrade the package and lockfile before adopting a policy file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,6 +26,8 @@
26
26
  "src/github-diff.ts",
27
27
  "src/github-settings.ts",
28
28
  "src/structure-check.ts",
29
+ "src/structure-profile.ts",
30
+ "src/structure-script.ts",
29
31
  "src/structure-workspace.ts"
30
32
  ],
31
33
  "scripts": {
package/src/cli.ts CHANGED
@@ -36,7 +36,8 @@ import { CANONICAL_SETTINGS_FILE, LOCAL_SETTINGS_FILE } from './github-api';
36
36
  import { runGithubApply, runGithubCheck } from './github-commands';
37
37
  import { loadGithubSettings } from './github-settings';
38
38
  import { collectStructureProblems } from './structure-check';
39
- import { hasSafeCommand } from './structure-workspace';
39
+ import type { StructureProfile } from './structure-profile';
40
+ import { hasSafeCommand } from './structure-script';
40
41
 
41
42
  const { YAML: BunYaml } = await import('bun');
42
43
 
@@ -92,6 +93,7 @@ type CliOptions = {
92
93
  readonly from: string | undefined;
93
94
  readonly ref: string | undefined;
94
95
  readonly apply: boolean;
96
+ readonly profile: StructureProfile;
95
97
  };
96
98
 
97
99
  const sha256 = (buf: Buffer): string =>
@@ -781,6 +783,7 @@ Commands:
781
783
 
782
784
  Options:
783
785
  --dir <path> Consumer directory to operate on (default: current directory)
786
+ --profile <p> With structure: validate as a "consumer" (default) or as the standards "source" repository itself
784
787
  --from <src> Upstream override for init/sync (GitHub repo or local path)
785
788
  --ref <ref> Upstream tag, branch, or full commit sha for init/sync (remote Git/GitHub sources only; default: main)
786
789
  --dry-run Preview a sync without writing anything
@@ -811,6 +814,13 @@ const setCommand = (current: Command | undefined, next: Command): Command => {
811
814
  return next;
812
815
  };
813
816
 
817
+ const structureProfileOf = (value: string): StructureProfile => {
818
+ if (value === 'consumer' || value === 'source') {
819
+ return value;
820
+ }
821
+ throw new Error(`--profile must be "consumer" or "source": ${value}`);
822
+ };
823
+
814
824
  const nextOptionValue = (
815
825
  argv: ReadonlyArray<string>,
816
826
  index: number,
@@ -830,6 +840,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
830
840
  let ref: string | undefined;
831
841
  let checkFlag = false;
832
842
  let apply = false;
843
+ let profile: StructureProfile | undefined;
833
844
 
834
845
  for (let index = 0; index < argv.length; index += 1) {
835
846
  const arg = argv[index];
@@ -851,6 +862,10 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
851
862
  from = nextOptionValue(argv, index);
852
863
  index += 1;
853
864
  break;
865
+ case '--profile':
866
+ profile = structureProfileOf(nextOptionValue(argv, index));
867
+ index += 1;
868
+ break;
854
869
  case '--ref':
855
870
  ref = nextOptionValue(argv, index);
856
871
  index += 1;
@@ -876,6 +891,9 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
876
891
  if (ref !== undefined && command !== 'init' && command !== 'sync') {
877
892
  throw new Error('--ref is only valid with the init and sync commands');
878
893
  }
894
+ if (profile !== undefined && command !== 'structure') {
895
+ throw new Error('--profile is only valid with the structure command');
896
+ }
879
897
 
880
898
  return {
881
899
  command,
@@ -884,13 +902,19 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
884
902
  from,
885
903
  ref,
886
904
  apply,
905
+ profile: profile ?? 'consumer',
887
906
  };
888
907
  };
889
908
 
890
909
  // Canonical monorepo structure gate: workspace and root script shapes,
891
910
  // internal versioning, `exports`, tsconfig inheritance, and a11y wiring.
892
- const runStructure = async (consumer: string): Promise<boolean> => {
893
- const problems = await collectStructureProblems(consumer);
911
+ // The `source` profile instead pins the standards template repository's own
912
+ // deliberate exceptions to the consumer contract.
913
+ const runStructure = async (
914
+ consumer: string,
915
+ profile: StructureProfile,
916
+ ): Promise<boolean> => {
917
+ const problems = await collectStructureProblems(consumer, profile);
894
918
  if (problems.length > 0) {
895
919
  console.error(
896
920
  `standards structure: ${problems.length} monorepo structure problem(s):`,
@@ -905,7 +929,7 @@ const runStructure = async (consumer: string): Promise<boolean> => {
905
929
  const runCheckCommand = async (consumer: string): Promise<boolean> => {
906
930
  const driftIsClean = await runCheck(consumer);
907
931
  const integrationIsValid = await runDoctor(consumer);
908
- const structureIsValid = await runStructure(consumer);
932
+ const structureIsValid = await runStructure(consumer, 'consumer');
909
933
  // The GitHub gate activates with the synced declaration and then fails
910
934
  // closed: once .github/settings.json exists, an unreachable API or an
911
935
  // unreadable origin is a failure, not a skip.
@@ -1048,6 +1072,7 @@ const runGateCommand = (
1048
1072
  command: 'check' | 'doctor' | 'github' | 'structure',
1049
1073
  consumer: string,
1050
1074
  apply: boolean,
1075
+ profile: StructureProfile,
1051
1076
  ): Promise<boolean> => {
1052
1077
  if (command === 'check') {
1053
1078
  return runCheckCommand(consumer);
@@ -1056,13 +1081,13 @@ const runGateCommand = (
1056
1081
  return runDoctor(consumer);
1057
1082
  }
1058
1083
  if (command === 'structure') {
1059
- return runStructure(consumer);
1084
+ return runStructure(consumer, profile);
1060
1085
  }
1061
1086
  return apply ? runGithubApply(consumer) : runGithubCheck(consumer);
1062
1087
  };
1063
1088
 
1064
1089
  const main = async (): Promise<void> => {
1065
- const { command, consumer, dryRun, from, ref, apply } = parseArgs(
1090
+ const { command, consumer, dryRun, from, ref, apply, profile } = parseArgs(
1066
1091
  process.argv.slice(2),
1067
1092
  );
1068
1093
 
@@ -1088,7 +1113,7 @@ const main = async (): Promise<void> => {
1088
1113
  return;
1089
1114
  }
1090
1115
 
1091
- if (!(await runGateCommand(command, consumer, apply))) {
1116
+ if (!(await runGateCommand(command, consumer, apply, profile))) {
1092
1117
  process.exitCode = 1;
1093
1118
  }
1094
1119
  };
@@ -3,22 +3,16 @@ import { readdir, readFile } from 'node:fs/promises';
3
3
  import { isAbsolute, join } from 'node:path';
4
4
  import { isRecord } from './github-settings';
5
5
  import {
6
- hasSafeCommand,
7
- inspectWorkspace,
8
- isSafeFilteredTurboAlias,
9
- type Workspace,
10
- } from './structure-workspace';
6
+ missingPublishedCliProblems,
7
+ rootScriptExpectations,
8
+ type StructureProfile,
9
+ } from './structure-profile';
10
+ import { hasSafeCommands, isSafeFilteredTurboAlias } from './structure-script';
11
+ import { inspectWorkspace, type Workspace } from './structure-workspace';
11
12
 
12
- const ROOT_CHECK = 'turbo run lint check-types test build test:a11y';
13
- const ROOT_CHECK_FIX = 'turbo run lint:fix check-types test build test:a11y';
14
- const ROOT_A11Y = 'turbo run test:a11y';
15
-
16
- const ROOT_FIXED_SCRIPTS = new Set([
17
- 'standards',
18
- 'check',
19
- 'check:fix',
20
- 'test:a11y',
21
- ]);
13
+ const ROOT_FIXED_SCRIPTS = new Set(
14
+ 'standards,check,check:fix,test:a11y'.split(','),
15
+ );
22
16
  const GLOB_SUFFIX = '/*';
23
17
  const WORKSPACES_REQUIREMENT =
24
18
  'package.json: "workspaces" must be a non-empty array of literal paths or one-level "<dir>/*" patterns';
@@ -136,19 +130,20 @@ const loadWorkspace = async (
136
130
  };
137
131
  const inspectRootScripts = (
138
132
  root: Record<string, unknown>,
133
+ profile: StructureProfile,
139
134
  requireA11y: boolean,
140
135
  ): ReadonlyArray<string> => {
141
136
  const scripts = isRecord(root.scripts) ? root.scripts : {};
142
- const expectations: ReadonlyArray<readonly [string, string]> = [
143
- ['check', ROOT_CHECK],
144
- ['check:fix', ROOT_CHECK_FIX],
145
- ...(requireA11y ? [['test:a11y', ROOT_A11Y] as const] : []),
146
- ];
147
- const gateProblems = expectations.flatMap(([name, fragment]) => {
137
+ const expectations = rootScriptExpectations(profile, requireA11y);
138
+ const gateProblems = expectations.flatMap(({ name, commands, exact }) => {
148
139
  const { [name]: script } = scripts;
149
- return typeof script === 'string' && hasSafeCommand(script, fragment)
140
+ const requirement = commands.join(' && ');
141
+ return typeof script === 'string' &&
142
+ hasSafeCommands(script, commands, exact)
150
143
  ? []
151
- : [`package.json: root script "${name}" must run ${fragment}`];
144
+ : [
145
+ `package.json: root script "${name}" must run ${exact ? 'exactly ' : ''}${requirement}`,
146
+ ];
152
147
  });
153
148
  const aliasProblems = Object.entries(scripts).flatMap(([name, script]) =>
154
149
  ROOT_FIXED_SCRIPTS.has(name) ||
@@ -163,6 +158,7 @@ const inspectRootScripts = (
163
158
  };
164
159
  export const collectStructureProblems = async (
165
160
  consumer: string,
161
+ profile: StructureProfile,
166
162
  ): Promise<ReadonlyArray<string>> => {
167
163
  const root = await readJson(join(consumer, 'package.json'));
168
164
  if (root === null) {
@@ -185,14 +181,15 @@ export const collectStructureProblems = async (
185
181
  .filter((name): name is string => typeof name === 'string'),
186
182
  );
187
183
  const inspections = await Promise.all(
188
- workspaces.map((ws) => inspectWorkspace(ws, workspaceNames)),
184
+ workspaces.map((ws) => inspectWorkspace(ws, workspaceNames, profile)),
189
185
  );
190
186
  const requireA11y = inspections.some((i) => i.hasA11ySuite);
191
187
  return [
192
188
  ...declaration.problems,
193
189
  ...resolved.flatMap((r) => (r.problem === null ? [] : [r.problem])),
194
190
  ...loaded.flatMap((l) => (l.problem === null ? [] : [l.problem])),
195
- ...inspectRootScripts(root, requireA11y),
191
+ ...missingPublishedCliProblems(profile, workspaces),
192
+ ...inspectRootScripts(root, profile, requireA11y),
196
193
  ...inspections.flatMap((i) => [...i.problems]),
197
194
  ];
198
195
  };
@@ -0,0 +1,129 @@
1
+ import { isRecord } from './github-settings';
2
+
3
+ // Structure validation profiles. `consumer` is the contract every downstream
4
+ // repo satisfies. `source` is the standards template repository itself, which
5
+ // is deliberately not a consumer: its root gate runs the local CLI instead of
6
+ // a recursive `standards check`, and it publishes the CLI workspace as a
7
+ // released bin-only package. Pinning those exceptions here keeps them from
8
+ // drifting silently while the normal quality gate stays green.
9
+ export type StructureProfile = 'consumer' | 'source';
10
+
11
+ const CONSUMER_CHECK = 'turbo run lint check-types test build test:a11y';
12
+ const CONSUMER_CHECK_FIX =
13
+ 'turbo run lint:fix check-types test build test:a11y';
14
+ const ROOT_A11Y = 'turbo run test:a11y';
15
+
16
+ export const PUBLISHED_CLI_WORKSPACE = 'packages/standards-cli';
17
+ const PUBLISHED_CLI_PACKAGE = '@davidvornholt/standards';
18
+ const SOURCE_CLI = `bun ${PUBLISHED_CLI_WORKSPACE}/src/cli.ts`;
19
+ const RELEASE_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u;
20
+
21
+ export type RootScriptExpectation = {
22
+ readonly name: string;
23
+ readonly commands: ReadonlyArray<string>;
24
+ readonly exact: boolean;
25
+ };
26
+
27
+ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
28
+ { name: 'standards', commands: [SOURCE_CLI], exact: true },
29
+ {
30
+ name: 'check',
31
+ commands: [
32
+ `${SOURCE_CLI} structure --profile source`,
33
+ `${SOURCE_CLI} github --check`,
34
+ 'turbo run lint check-types test',
35
+ ],
36
+ exact: true,
37
+ },
38
+ {
39
+ name: 'check:fix',
40
+ commands: [
41
+ `${SOURCE_CLI} structure --profile source`,
42
+ `${SOURCE_CLI} github --check`,
43
+ 'turbo run lint:fix check-types test',
44
+ ],
45
+ exact: true,
46
+ },
47
+ ];
48
+
49
+ export const rootScriptExpectations = (
50
+ profile: StructureProfile,
51
+ requireA11y: boolean,
52
+ ): ReadonlyArray<RootScriptExpectation> => [
53
+ ...(profile === 'source'
54
+ ? SOURCE_ROOT_EXPECTATIONS
55
+ : [
56
+ { name: 'check', commands: [CONSUMER_CHECK], exact: false },
57
+ { name: 'check:fix', commands: [CONSUMER_CHECK_FIX], exact: false },
58
+ ]),
59
+ ...(requireA11y
60
+ ? [{ name: 'test:a11y', commands: [ROOT_A11Y], exact: false }]
61
+ : []),
62
+ ];
63
+
64
+ // The published CLI ships as a released bin-only package: it carries a release
65
+ // SemVer instead of the internal "0.0.0" and exposes its public API through
66
+ // "bin" instead of "exports". Everything else follows the normal rules.
67
+ const inspectPublishedCli = (
68
+ manifest: Record<string, unknown>,
69
+ ): ReadonlyArray<string> => [
70
+ ...(manifest.name === PUBLISHED_CLI_PACKAGE
71
+ ? []
72
+ : [
73
+ `${PUBLISHED_CLI_WORKSPACE}: published CLI package name must be "${PUBLISHED_CLI_PACKAGE}"`,
74
+ ]),
75
+ ...(typeof manifest.version === 'string' &&
76
+ manifest.version !== '0.0.0' &&
77
+ RELEASE_SEMVER.test(manifest.version)
78
+ ? []
79
+ : [
80
+ `${PUBLISHED_CLI_WORKSPACE}: published CLI version must be a stable release SemVer, not "0.0.0"`,
81
+ ]),
82
+ ...(isRecord(manifest.bin) &&
83
+ Object.keys(manifest.bin).length === 1 &&
84
+ manifest.bin.standards === 'src/cli.ts'
85
+ ? []
86
+ : [
87
+ `${PUBLISHED_CLI_WORKSPACE}: published CLI bin must be exactly { "standards": "src/cli.ts" }`,
88
+ ]),
89
+ ...(manifest.private === true
90
+ ? [`${PUBLISHED_CLI_WORKSPACE}: published CLI must not be private`]
91
+ : []),
92
+ ...(manifest.exports === undefined
93
+ ? []
94
+ : [
95
+ `${PUBLISHED_CLI_WORKSPACE}: published CLI must be bin-only and must not define "exports"`,
96
+ ]),
97
+ ];
98
+
99
+ export const inspectVersionAndExports = (
100
+ profile: StructureProfile,
101
+ rel: string,
102
+ manifest: Record<string, unknown>,
103
+ ): ReadonlyArray<string> => {
104
+ if (profile === 'source' && rel === PUBLISHED_CLI_WORKSPACE) {
105
+ return inspectPublishedCli(manifest);
106
+ }
107
+ return [
108
+ ...(manifest.version === '0.0.0'
109
+ ? []
110
+ : [`${rel}: internal workspace version must be "0.0.0"`]),
111
+ ...(rel.startsWith('packages/') && manifest.exports === undefined
112
+ ? [`${rel}: package must define its public API with "exports"`]
113
+ : []),
114
+ ];
115
+ };
116
+
117
+ // The source profile only means anything in the repository that owns and
118
+ // publishes the CLI; failing loudly when that workspace is absent keeps the
119
+ // profile from being applied to the wrong repository shape.
120
+ export const missingPublishedCliProblems = (
121
+ profile: StructureProfile,
122
+ workspaces: ReadonlyArray<{ readonly rel: string }>,
123
+ ): ReadonlyArray<string> =>
124
+ profile === 'source' &&
125
+ !workspaces.some((ws) => ws.rel === PUBLISHED_CLI_WORKSPACE)
126
+ ? [
127
+ `${PUBLISHED_CLI_WORKSPACE}: the source profile requires the published CLI workspace`,
128
+ ]
129
+ : [];
@@ -0,0 +1,63 @@
1
+ const UNSAFE_SCRIPT_SYNTAX = /[|;#"'`\r\n]/u;
2
+ const SCRIPT_WHITESPACE = /\s+/u;
3
+ const NON_EXECUTING_TURBO_OPTION = /^(?:-h|-v|--(?:dry|help|version))(?:=|$)/u;
4
+
5
+ export const parseSafeCommands = (
6
+ script: string | null,
7
+ ): ReadonlyArray<ReadonlyArray<string>> | null => {
8
+ if (
9
+ script === null ||
10
+ script.trim() === '' ||
11
+ UNSAFE_SCRIPT_SYNTAX.test(script) ||
12
+ script.includes('$(')
13
+ ) {
14
+ return null;
15
+ }
16
+ const commands = script.split('&&').map((command) => command.trim());
17
+ return commands.some((command) => command === '' || command.includes('&'))
18
+ ? null
19
+ : commands.map((command) => command.split(SCRIPT_WHITESPACE));
20
+ };
21
+
22
+ export const hasSafeCommands = (
23
+ script: string | null,
24
+ expected: ReadonlyArray<string>,
25
+ exact: boolean,
26
+ ): boolean => {
27
+ const commands = parseSafeCommands(script)?.map((tokens) => tokens.join(' '));
28
+ if (commands === undefined) {
29
+ return false;
30
+ }
31
+ return exact
32
+ ? commands.length === expected.length &&
33
+ commands.every((command, index) => command === expected[index])
34
+ : expected.every((command) => commands.includes(command));
35
+ };
36
+
37
+ export const hasSafeCommand = (
38
+ script: string | null,
39
+ expected: string,
40
+ ): boolean => hasSafeCommands(script, [expected], false);
41
+
42
+ export const isSafeFilteredTurboAlias = (script: string): boolean => {
43
+ const commands = parseSafeCommands(script);
44
+ if (commands?.length !== 1) {
45
+ return false;
46
+ }
47
+ const [tokens] = commands;
48
+ const filterAt = tokens.findIndex(
49
+ (token) => token === '--filter' || token.startsWith('--filter='),
50
+ );
51
+ const filter = tokens[filterAt];
52
+ const filterValue =
53
+ filter === '--filter'
54
+ ? tokens[filterAt + 1]
55
+ : filter?.slice('--filter='.length);
56
+ return tokens[0] !== 'turbo' || tokens[1] !== 'run'
57
+ ? false
58
+ : tokens[2]?.startsWith('-') === false &&
59
+ !tokens.some((token) => NON_EXECUTING_TURBO_OPTION.test(token)) &&
60
+ filterValue !== undefined &&
61
+ filterValue !== '' &&
62
+ !filterValue.startsWith('-');
63
+ };
@@ -1,6 +1,11 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { isRecord } from './github-settings';
4
+ import {
5
+ inspectVersionAndExports,
6
+ type StructureProfile,
7
+ } from './structure-profile';
8
+ import { hasSafeCommand, parseSafeCommands } from './structure-script';
4
9
  export type Workspace = {
5
10
  readonly rel: string;
6
11
  readonly dir: string;
@@ -20,11 +25,8 @@ const DEPENDENCY_FIELDS =
20
25
  const SHARED_TSCONFIG_PACKAGE = '@davidvornholt/typescript-config';
21
26
  const SHARED_TSCONFIG_PATH =
22
27
  /^@davidvornholt\/typescript-config\/(?:base|next|react-library)$/u;
23
- const UNSAFE_SCRIPT_SYNTAX = /[|;#"'`\r\n]/u;
24
- const SCRIPT_WHITESPACE = /\s+/u;
25
28
  const NON_EXECUTING_A11Y_OPTION =
26
29
  /^(?:-h|-V|--(?:debug|help|last-failed|list|only-changed|ui|version))(?:=|$)/u;
27
- const NON_EXECUTING_TURBO_OPTION = /^(?:-h|-v|--(?:dry|help|version))(?:=|$)/u;
28
30
  const IGNORED_DIRS = new Set('.git,.next,.turbo,dist,node_modules'.split(','));
29
31
  const scriptOf = (
30
32
  manifest: Record<string, unknown>,
@@ -34,57 +36,13 @@ const scriptOf = (
34
36
  const script = scripts[name];
35
37
  return typeof script === 'string' ? script : null;
36
38
  };
37
- const safeCommands = (
38
- script: string | null,
39
- ): ReadonlyArray<ReadonlyArray<string>> | null => {
40
- if (
41
- script === null ||
42
- script.trim() === '' ||
43
- UNSAFE_SCRIPT_SYNTAX.test(script) ||
44
- script.includes('$(')
45
- ) {
46
- return null;
47
- }
48
- const commands = script.split('&&').map((command) => command.trim());
49
- return commands.some((command) => command === '' || command.includes('&'))
50
- ? null
51
- : commands.map((command) => command.split(SCRIPT_WHITESPACE));
52
- };
53
- export const hasSafeCommand = (
54
- script: string | null,
55
- expected: string,
56
- ): boolean =>
57
- safeCommands(script)?.some((tokens) => tokens.join(' ') === expected) ??
58
- false;
59
39
  const hasSafeA11yCommand = (script: string | null): boolean =>
60
- safeCommands(script)?.some(
40
+ parseSafeCommands(script)?.some(
61
41
  (tokens) =>
62
42
  tokens[0] === 'playwright' &&
63
43
  tokens[1] === 'test' &&
64
44
  tokens.slice(2).every((token) => !NON_EXECUTING_A11Y_OPTION.test(token)),
65
45
  ) ?? false;
66
- export const isSafeFilteredTurboAlias = (script: string): boolean => {
67
- const commands = safeCommands(script);
68
- if (commands?.length !== 1) {
69
- return false;
70
- }
71
- const [tokens] = commands;
72
- const filterAt = tokens.findIndex(
73
- (token) => token === '--filter' || token.startsWith('--filter='),
74
- );
75
- const filter = tokens[filterAt];
76
- const filterValue =
77
- filter === '--filter'
78
- ? tokens[filterAt + 1]
79
- : filter?.slice('--filter='.length);
80
- return tokens[0] !== 'turbo' || tokens[1] !== 'run'
81
- ? false
82
- : tokens[2]?.startsWith('-') === false &&
83
- !tokens.some((token) => NON_EXECUTING_TURBO_OPTION.test(token)) &&
84
- filterValue !== undefined &&
85
- filterValue !== '' &&
86
- !filterValue.startsWith('-');
87
- };
88
46
  const containsA11ySuite = async (dir: string): Promise<boolean> => {
89
47
  const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
90
48
  if (
@@ -179,6 +137,7 @@ const inspectA11y = async (
179
137
  export const inspectWorkspace = async (
180
138
  ws: Workspace,
181
139
  workspaceNames: ReadonlySet<string>,
140
+ profile: StructureProfile,
182
141
  ) => {
183
142
  const [tsconfigProblems, a11y] = await Promise.all([
184
143
  inspectTsconfig(ws),
@@ -186,13 +145,8 @@ export const inspectWorkspace = async (
186
145
  ]);
187
146
  const problems = [
188
147
  ...inspectScripts(ws),
189
- ...(ws.manifest.version === '0.0.0'
190
- ? []
191
- : [`${ws.rel}: internal workspace version must be "0.0.0"`]),
148
+ ...inspectVersionAndExports(profile, ws.rel, ws.manifest),
192
149
  ...inspectInternalDeps(ws, workspaceNames),
193
- ...(ws.rel.startsWith('packages/') && ws.manifest.exports === undefined
194
- ? [`${ws.rel}: package must define its public API with "exports"`]
195
- : []),
196
150
  ...tsconfigProblems,
197
151
  ...a11y.problems,
198
152
  ];