@smoothbricks/cli 0.11.19 → 0.11.21

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.
Files changed (84) hide show
  1. package/README.md +68 -5
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +28 -4
  4. package/dist/lib/json.d.ts +22 -1
  5. package/dist/lib/json.d.ts.map +1 -1
  6. package/dist/lib/json.js +15 -3
  7. package/dist/monorepo/cargo-policy.d.ts +17 -0
  8. package/dist/monorepo/cargo-policy.d.ts.map +1 -1
  9. package/dist/monorepo/cargo-policy.js +73 -1
  10. package/dist/monorepo/index.d.ts.map +1 -1
  11. package/dist/monorepo/index.js +4 -2
  12. package/dist/monorepo/packs/index.d.ts.map +1 -1
  13. package/dist/monorepo/packs/index.js +6 -2
  14. package/dist/nx/index.d.ts +6 -1
  15. package/dist/nx/index.d.ts.map +1 -1
  16. package/dist/nx/index.js +14 -1
  17. package/dist/secrets/commands.d.ts +9 -5
  18. package/dist/secrets/commands.d.ts.map +1 -1
  19. package/dist/secrets/commands.js +122 -49
  20. package/dist/secrets/index.d.ts +42 -56
  21. package/dist/secrets/index.d.ts.map +1 -1
  22. package/dist/secrets/index.js +52 -79
  23. package/dist/secrets/resolver.d.ts +59 -0
  24. package/dist/secrets/resolver.d.ts.map +1 -0
  25. package/dist/secrets/resolver.js +100 -0
  26. package/dist/secrets/run.d.ts +23 -0
  27. package/dist/secrets/run.d.ts.map +1 -0
  28. package/dist/secrets/run.js +130 -0
  29. package/dist/secrets/status.d.ts +177 -0
  30. package/dist/secrets/status.d.ts.map +1 -0
  31. package/dist/secrets/status.js +724 -0
  32. package/dist/wrangler/deploy-stage.d.ts +1 -1
  33. package/dist/wrangler/deploy-stage.d.ts.map +1 -1
  34. package/dist/wrangler/deploy-stage.js +22 -20
  35. package/dist/wrangler/deployed-version.d.ts.map +1 -1
  36. package/dist/wrangler/deployed-version.js +7 -12
  37. package/dist/wrangler/flat-config.d.ts +1 -4
  38. package/dist/wrangler/flat-config.d.ts.map +1 -1
  39. package/dist/wrangler/flat-config.js +98 -44
  40. package/dist/wrangler/prepare-env.d.ts +4 -3
  41. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  42. package/dist/wrangler/prepare-env.js +7 -5
  43. package/dist/wrangler/source-config.d.ts +24 -0
  44. package/dist/wrangler/source-config.d.ts.map +1 -0
  45. package/dist/wrangler/source-config.js +110 -0
  46. package/dist/wrangler/stage.d.ts +55 -23
  47. package/dist/wrangler/stage.d.ts.map +1 -1
  48. package/dist/wrangler/stage.js +81 -162
  49. package/managed/raw/tooling/direnv/devenv.smoo.nix +19 -3
  50. package/managed/raw/tooling/direnv/secret-references.ts +280 -76
  51. package/managed/raw/tooling/direnv/setup-environment.ts +58 -2
  52. package/managed/raw/tsconfig.lib.json +28 -0
  53. package/package.json +7 -2
  54. package/src/cli.ts +34 -5
  55. package/src/lib/json.ts +23 -1
  56. package/src/monorepo/cargo-policy.test.ts +91 -1
  57. package/src/monorepo/cargo-policy.ts +87 -1
  58. package/src/monorepo/index.ts +8 -2
  59. package/src/monorepo/package-policy.test.ts +50 -19
  60. package/src/monorepo/packs/index.ts +10 -2
  61. package/src/monorepo/secret-references.test.ts +412 -6
  62. package/src/monorepo/setup-environment.test.ts +181 -0
  63. package/src/nx/index.test.ts +8 -2
  64. package/src/nx/index.ts +20 -2
  65. package/src/secrets/commands.test.ts +186 -4
  66. package/src/secrets/commands.ts +142 -51
  67. package/src/secrets/index.test.ts +4 -10
  68. package/src/secrets/index.ts +54 -115
  69. package/src/secrets/resolver.ts +130 -0
  70. package/src/secrets/run.test.ts +359 -0
  71. package/src/secrets/run.ts +148 -0
  72. package/src/secrets/status.test.ts +164 -0
  73. package/src/secrets/status.ts +297 -0
  74. package/src/wrangler/deploy-stage.test.ts +95 -2
  75. package/src/wrangler/deploy-stage.ts +26 -23
  76. package/src/wrangler/deployed-version.ts +7 -11
  77. package/src/wrangler/flat-config.test.ts +9 -4
  78. package/src/wrangler/flat-config.ts +1 -20
  79. package/src/wrangler/format-parity.test.ts +433 -0
  80. package/src/wrangler/prepare-env.ts +7 -5
  81. package/src/wrangler/source-config.test.ts +102 -0
  82. package/src/wrangler/source-config.ts +110 -0
  83. package/src/wrangler/stage.test.ts +61 -32
  84. package/src/wrangler/stage.ts +124 -173
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@ import { decode, findRepoRoot, printCommandOutput } from './lib/run.js';
7
7
  import { ensureChromium } from './playwright/index.js';
8
8
  import { resolvePrConflicts } from './pr/index.js';
9
9
  import { secretsSet, secretsStatus, secretsSync } from './secrets/commands.js';
10
+ import { secretsRun } from './secrets/run.js';
10
11
  import { cleanupPullRequest, deployStage } from './wrangler/deploy-stage.js';
11
12
  import { deployedVersion } from './wrangler/deployed-version.js';
12
13
  import { scaffold } from './wrangler/scaffold.js';
@@ -74,6 +75,11 @@ function buildProgram(): Command {
74
75
  .description('SmoothBricks monorepo tooling')
75
76
  .version(cliPackageVersion, '-v, --version', 'print smoo version')
76
77
  .exitOverride()
78
+ // `smoo secrets run <group> <command...>` hands the rest of argv to a
79
+ // child verbatim, flags included. Commander only stops parsing after the
80
+ // first operand when positional options are enabled here, on the parent
81
+ // of the command that declares `passThroughOptions`.
82
+ .enablePositionalOptions()
77
83
  .showHelpAfterError();
78
84
 
79
85
  const monorepo = program.command('monorepo').description('Manage SmoothBricks-style monorepos');
@@ -529,11 +535,29 @@ function buildProgram(): Command {
529
535
 
530
536
  const secrets = program
531
537
  .command('secrets')
532
- .description('Reconcile declared secrets: what Workers need, what workflows pass, what the repository holds');
538
+ .description('Reconcile declared secrets: what Workers need, what workflows pass, what the repository holds')
539
+ .enablePositionalOptions();
540
+ secrets
541
+ .command('run [group] [command...]')
542
+ .description(
543
+ 'Run one command with one group of declared secrets in its environment. Shell entry resolves the ' +
544
+ '`shell` group only, so a registry credential or the Nx cache token is resolved here, by the command ' +
545
+ 'that needs it, instead of by every direnv reload. The group is required and positional: with none, ' +
546
+ 'this lists the groups this repository declares and the secrets in each',
547
+ )
548
+ // Everything after the group reaches the child verbatim, flags included:
549
+ // `smoo secrets run registry bun add -d @acme/x` must run `bun add -d
550
+ // @acme/x`, not lose `-d` to this command's own parser.
551
+ .passThroughOptions()
552
+ .action(async (group: string | undefined, command: string[]) => {
553
+ process.exitCode = await secretsRun(await findRepoRoot(), group, command);
554
+ });
533
555
  secrets
534
556
  .command('status')
535
557
  .description(
536
- 'Show every declared secret with the scopes holding it, and refuse when a workflow passes one none has',
558
+ 'Show every declared secret with the scopes holding it. Exits 0 when every secret a managed workflow ' +
559
+ 'passes has a value in a scope the job reads, and 1 when one does not or when the repository, its ' +
560
+ 'secrets or a stage declaration could not be read',
537
561
  )
538
562
  .option(
539
563
  '-R, --repo <owner/name|remote>',
@@ -543,8 +567,13 @@ function buildProgram(): Command {
543
567
  '--env <environment>',
544
568
  "also read this GitHub Environment; its value takes precedence over the repository's for a job bound to it",
545
569
  )
546
- .action(async (options: { repo?: string; env?: string }) => {
547
- process.exitCode = secretsStatus(await findRepoRoot(), options);
570
+ .option(
571
+ '--json',
572
+ 'write one JSON document to stdout instead of the table, for another tool to read; the exit code is ' +
573
+ 'unchanged, so a status that refuses still refuses',
574
+ )
575
+ .action(async (options: { repo?: string; env?: string; json?: boolean }) => {
576
+ process.exitCode = await secretsStatus(await findRepoRoot(), options);
548
577
  });
549
578
  secrets
550
579
  .command('set [name]')
@@ -586,7 +615,7 @@ function buildProgram(): Command {
586
615
  wrangler
587
616
  .command('deploy-stage')
588
617
  .requiredOption('--stage <stage>', 'staging, production, or prN')
589
- .option('--config <path>', 'deploy a build-generated flat wrangler.json instead of ./wrangler.toml')
618
+ .option('--config <path>', "deploy a build-generated flat wrangler.json instead of the project's own config")
590
619
  .option('--version-endpoint <url>', 'URL served by this worker whose trimmed body is the running version tag')
591
620
  .action(async (options: { stage: string; config?: string; versionEndpoint?: string }) => {
592
621
  await deployStage(process.cwd(), {
package/src/lib/json.ts CHANGED
@@ -212,10 +212,32 @@ export interface PackageRemoteCacheConfig {
212
212
  tokenSecret: RepositorySecretName;
213
213
  }
214
214
 
215
- /** Local bootstrap fallback; existing environment values always take precedence. */
215
+ /**
216
+ * A `smoo.secrets` group: which operation resolves the credential. One argv
217
+ * word of `smoo secrets run <group> <command...>`, so whitespace would make
218
+ * that command unwritable. Only the shape is validated — the values are the
219
+ * repository's, and a new group must not need a new smoo release.
220
+ */
221
+ export type SecretGroupName = string & typia.tags.MinLength<1> & typia.tags.Pattern<'^\\S+$'>;
222
+
223
+ /**
224
+ * Local bootstrap fallback; existing environment values always take
225
+ * precedence, and CI injects these variables instead of running commands.
226
+ *
227
+ * Shell entry resolves the `shell` group and nothing else; every other group
228
+ * is resolved by `smoo secrets run <group> <command...>`, the one command
229
+ * that needs it. The group is DERIVED from what this manifest already
230
+ * declares — a variable `.npmrc` interpolates as `${VAR}` is `registry`, the
231
+ * variable `smoo.remoteCache.tokenSecret` names is `nx-cache`, everything
232
+ * else is `shell` — so `group` below is only for what those declarations
233
+ * cannot say. The routing lives in tooling/direnv/secret-references.ts,
234
+ * whose header states the rule and the derivation.
235
+ */
216
236
  export interface PackageSecretCommand {
217
237
  /** Executed directly, without a shell; stdout supplies the secret value. */
218
238
  command: NonEmptyArray<string>;
239
+ /** Overrides the derived group; absent derives one from this manifest and `.npmrc`. */
240
+ group?: SecretGroupName;
219
241
  }
220
242
 
221
243
  export interface PackageSmooConfig {
@@ -3,7 +3,13 @@ import { readFileSync } from 'node:fs';
3
3
  import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
- import { applyCargoFeatureUnification, type CargoHakariShell, validateCargoCachePolicy } from './cargo-policy.js';
6
+ import type { ProjectTargets } from '../nx/index.js';
7
+ import {
8
+ applyCargoFeatureUnification,
9
+ type CargoHakariShell,
10
+ validateCargoCachePolicy,
11
+ validateCargoToolchainInputs,
12
+ } from './cargo-policy.js';
7
13
 
8
14
  async function withFixture<T>(files: Record<string, string>, callback: (root: string) => T): Promise<T> {
9
15
  const root = await mkdtemp(join(tmpdir(), 'smoo-cargo-policy-'));
@@ -33,6 +39,20 @@ async function check(
33
39
  });
34
40
  }
35
41
 
42
+ async function checkToolchain(
43
+ files: Record<string, string>,
44
+ projects: ProjectTargets[],
45
+ ): Promise<{ failures: number; messages: string[] }> {
46
+ return withFixture(files, (root) => {
47
+ const captured = captureErrors();
48
+ try {
49
+ return { failures: validateCargoToolchainInputs(root, projects), messages: captured.messages };
50
+ } finally {
51
+ captured.restore();
52
+ }
53
+ });
54
+ }
55
+
36
56
  /** Records what update would run, and answers `verify` however the test needs. */
37
57
  function recordingHakari(
38
58
  verify: { code: number; output?: string; missing?: boolean } = { code: 0 },
@@ -389,3 +409,73 @@ describe('Cargo feature unification update', () => {
389
409
  );
390
410
  });
391
411
  });
412
+
413
+ describe('cargo toolchain identity policy', () => {
414
+ const cargoLint = (inputs: string[]): ProjectTargets => ({
415
+ project: 'codebase',
416
+ root: '.',
417
+ targets: ['cargo-lint'],
418
+ targetCache: new Map([['cargo-lint', true]]),
419
+ targetInputs: new Map([['cargo-lint', inputs]]),
420
+ targetOptions: new Map([['cargo-lint', { command: 'cargo --frozen clippy --workspace -- -D warnings' }]]),
421
+ });
422
+
423
+ // The failure this policy exists for: a hand-written input list replaces the
424
+ // inferred one, so the target stops hashing the pin and a toolchain bump
425
+ // leaves its cached verdict standing.
426
+ it('refuses a cached cargo target whose declared inputs drop the pin', async () => {
427
+ const { failures, messages } = await checkToolchain({ 'nx.json': '{}\n' }, [cargoLint(['rustWorkspace'])]);
428
+ expect(failures).toBe(1);
429
+ expect(messages).toEqual([
430
+ 'codebase:cargo-lint: cached cargo target hashes no toolchain pin, so a toolchain bump cannot invalidate it. ' +
431
+ 'Add "cargoToolchain" to its inputs, or to the named input it uses.',
432
+ ]);
433
+ });
434
+
435
+ it('accepts the pin reached directly, through a named input, or by the name inference defines', async () => {
436
+ const viaNamedInput = JSON.stringify({
437
+ namedInputs: { rustWorkspace: ['{workspaceRoot}/Cargo.toml', '{workspaceRoot}/tooling/direnv/devenv.lock'] },
438
+ });
439
+ expect(await checkToolchain({ 'nx.json': viaNamedInput }, [cargoLint(['rustWorkspace'])])).toMatchObject({
440
+ failures: 0,
441
+ });
442
+ expect(
443
+ await checkToolchain({ 'nx.json': '{}\n' }, [cargoLint(['{workspaceRoot}/tooling/direnv/devenv.lock'])]),
444
+ ).toMatchObject({ failures: 0 });
445
+ expect(await checkToolchain({ 'nx.json': '{}\n' }, [cargoLint(['cargoToolchain'])])).toMatchObject({
446
+ failures: 0,
447
+ });
448
+ });
449
+
450
+ // A negated fileset REMOVES a path from the hash. Reading one as the pin
451
+ // would accept the exact declaration that guarantees the bug.
452
+ it('does not accept an excluded lock as the pin', async () => {
453
+ expect(
454
+ await checkToolchain({ 'nx.json': '{}\n' }, [cargoLint(['!{workspaceRoot}/tooling/direnv/devenv.lock'])]),
455
+ ).toMatchObject({ failures: 1 });
456
+ });
457
+
458
+ // Only what Nx can restore, and only what runs cargo: an uncached warm-up or
459
+ // a TypeScript build has no stale artifact for a toolchain bump to strand.
460
+ it('governs cached cargo targets only', async () => {
461
+ const uncached: ProjectTargets = {
462
+ ...cargoLint([]),
463
+ targetCache: new Map([['cargo-lint', false]]),
464
+ };
465
+ const notCargo: ProjectTargets = {
466
+ ...cargoLint([]),
467
+ targets: ['tsc-js'],
468
+ targetCache: new Map([['tsc-js', true]]),
469
+ targetInputs: new Map([['tsc-js', ['default']]]),
470
+ targetOptions: new Map([['tsc-js', { command: 'ttsc --build' }]]),
471
+ };
472
+ expect(await checkToolchain({ 'nx.json': '{}\n' }, [uncached, notCargo])).toMatchObject({ failures: 0 });
473
+ });
474
+
475
+ // A cyclic named input is a repository mistake, not a reason to hang the
476
+ // whole validation run before it can report anything at all.
477
+ it('terminates on a named input that references itself', async () => {
478
+ const cyclic = JSON.stringify({ namedInputs: { rustWorkspace: ['rustWorkspace'] } });
479
+ expect(await checkToolchain({ 'nx.json': cyclic }, [cargoLint(['rustWorkspace'])])).toMatchObject({ failures: 1 });
480
+ });
481
+ });
@@ -1,8 +1,10 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { type Dirent, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { CARGO_TOOLCHAIN_NAMED_INPUT, isCargoToolchainInput } from '@smoothbricks/nx-plugin/cargo-toolchain-policy';
4
5
  import typia from 'typia';
5
- import { parsePackageJsonText } from '../lib/json.js';
6
+ import { type NxJson, type NxTargetOptions, parseNxJsonText, parsePackageJsonText } from '../lib/json.js';
7
+ import type { ProjectTargets } from '../nx/index.js';
6
8
 
7
9
  interface CargoProfile {
8
10
  inherits?: string;
@@ -869,6 +871,90 @@ export function validateCargoCachePolicy(root: string, options: CargoPolicyOptio
869
871
  return failures;
870
872
  }
871
873
 
874
+ /** A command that compiles or checks Rust, whichever driver spells it. */
875
+ const RUNS_CARGO = /(?:^|[\s;&|])cargo[\s-]|\bnapi build\b/;
876
+
877
+ const CARGO_TOOLCHAIN_FIX = `Add "${CARGO_TOOLCHAIN_NAMED_INPUT}" to its inputs, or to the named input it uses`;
878
+
879
+ function commandsOf(options: NxTargetOptions | undefined): string {
880
+ const command: unknown = options?.command;
881
+ const commands: unknown = options?.commands;
882
+ return [
883
+ typeof command === 'string' ? command : '',
884
+ ...(Array.isArray(commands) ? commands.filter((entry) => typeof entry === 'string') : []),
885
+ ].join('\n');
886
+ }
887
+
888
+ /**
889
+ * Every fileset one declared input list reaches, with nx.json's named inputs
890
+ * expanded. The plugin's own `cargoToolchain` is defined per PROJECT and wins
891
+ * over nx.json, so naming it counts without an entry here; a repository that
892
+ * redefines it workspace-wide is expanded and judged on what it resolves to.
893
+ */
894
+ function resolveInputFilesets(
895
+ declared: readonly string[],
896
+ namedInputs: NonNullable<NxJson['namedInputs']>,
897
+ seen: Set<string> = new Set(),
898
+ ): string[] {
899
+ const filesets: string[] = [];
900
+ for (const entry of declared) {
901
+ const name = entry.startsWith('^') ? entry.slice(1) : entry;
902
+ const definition = namedInputs[name];
903
+ if (definition === undefined) {
904
+ filesets.push(entry);
905
+ continue;
906
+ }
907
+ if (seen.has(name)) continue;
908
+ seen.add(name);
909
+ const expanded = typeof definition === 'string' ? [definition] : definition;
910
+ filesets.push(
911
+ ...resolveInputFilesets(
912
+ expanded.filter((value): value is string => typeof value === 'string'),
913
+ namedInputs,
914
+ seen,
915
+ ),
916
+ );
917
+ }
918
+ return filesets;
919
+ }
920
+
921
+ /**
922
+ * A cached cargo target must hash the toolchain that compiled it.
923
+ *
924
+ * Inference attaches the pin to every cargo target it infers, but a repository
925
+ * that DECLARES `inputs` for one replaces that list wholesale and silently
926
+ * drops it — the target then survives a toolchain bump with a stale verdict,
927
+ * and a pre-push cross-compile probe consults a cache entry that cannot
928
+ * express the change it is probing for. Measured before this policy existed:
929
+ * a one-byte bump to the declared pin left one repository's `cargo-lint-cross`
930
+ * hash unchanged.
931
+ *
932
+ * This is a generated safety property removed by a local override, which no
933
+ * amount of care at the override site will catch on its own, so it is checked
934
+ * against the RESOLVED graph rather than against any one file.
935
+ */
936
+ export function validateCargoToolchainInputs(root: string, projects: readonly ProjectTargets[]): number {
937
+ const nxJsonPath = join(resolve(root), 'nx.json');
938
+ const namedInputs = existsSync(nxJsonPath)
939
+ ? (parseNxJsonText(readFileSync(nxJsonPath, 'utf8'))?.namedInputs ?? {})
940
+ : {};
941
+ let failures = 0;
942
+ for (const project of projects) {
943
+ for (const target of [...project.targets].sort()) {
944
+ if (project.targetCache?.get(target) !== true) continue;
945
+ if (!RUNS_CARGO.test(commandsOf(project.targetOptions?.get(target)))) continue;
946
+ const declared = project.targetInputs?.get(target) ?? [];
947
+ if (declared.includes(CARGO_TOOLCHAIN_NAMED_INPUT)) continue;
948
+ if (resolveInputFilesets(declared, namedInputs).some(isCargoToolchainInput)) continue;
949
+ failures += report(
950
+ `${project.project}:${target}`,
951
+ `cached cargo target hashes no toolchain pin, so a toolchain bump cannot invalidate it. ${CARGO_TOOLCHAIN_FIX}.`,
952
+ );
953
+ }
954
+ }
955
+ return failures;
956
+ }
957
+
872
958
  /**
873
959
  * The fix `smoo monorepo update` applies for the policy above: write the
874
960
  * nightly resolver configuration, or generate and wire the workspace-hack a
@@ -2,7 +2,11 @@ import { appendFileSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { printCommandOutput, run, runResult } from '../lib/run.js';
3
3
  import { escapeRegex, getWorkspacePackages, getWorkspacePatterns, listReleasePackages } from '../lib/workspace.js';
4
4
  import { readProjectTargets } from '../nx/index.js';
5
- import { applyCargoFeatureUnification, validateCargoCachePolicy } from './cargo-policy.js';
5
+ import {
6
+ applyCargoFeatureUnification,
7
+ validateCargoCachePolicy,
8
+ validateCargoToolchainInputs,
9
+ } from './cargo-policy.js';
6
10
  import {
7
11
  formatCommitMessage,
8
12
  stagedDeletedPublicPackages,
@@ -133,7 +137,8 @@ export async function checkManagedFiles(root: string, options: { warn?: boolean
133
137
  }
134
138
  const results = await applyManagedFiles(root, 'check');
135
139
  printResults(results);
136
- const resolvedTargets = resolvedTargetsByProject(await readProjectTargets(root));
140
+ const projectTargets = await readProjectTargets(root);
141
+ const resolvedTargets = resolvedTargetsByProject(projectTargets);
137
142
  const packageFailures =
138
143
  validateRootPackagePolicy(root) +
139
144
  validateNxProjectNames(root) +
@@ -145,6 +150,7 @@ export async function checkManagedFiles(root: string, options: { warn?: boolean
145
150
  validateWorkspaceDependencies(root, { resolvedTargetsByProject: resolvedTargets }) +
146
151
  validateDevenvModuleImport(root) +
147
152
  validateCargoCachePolicy(root) +
153
+ validateCargoToolchainInputs(root, projectTargets) +
148
154
  validateSccachePatches(root);
149
155
  if (results.some((result) => result.action === 'drifted') || packageFailures > 0) {
150
156
  throw new Error('Managed monorepo files or package conventions are out of date. Run: smoo monorepo update');
@@ -532,12 +532,7 @@ describe('workspace package script policy', () => {
532
532
  });
533
533
  try {
534
534
  await writeJson(join(root, 'packages/native/tsconfig.lib.json'), {});
535
- const resolvedTargetsByProject = new Map([
536
- [
537
- 'native',
538
- { targets: new Set(['build', 'tsc-js', 'tsdown-js']), buildDependsOn: ['^build', 'tsc-js', 'tsdown-js'] },
539
- ],
540
- ]);
535
+ const resolvedTargetsByProject = new Map([['native', { targets: new Set(['build', 'tsc-js', 'tsdown-js']) }]]);
541
536
 
542
537
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
543
538
 
@@ -574,9 +569,7 @@ describe('workspace package script policy', () => {
574
569
  ],
575
570
  });
576
571
  try {
577
- const resolvedTargetsByProject = new Map([
578
- ['native', { targets: new Set(['build', 'tsdown-js']), buildDependsOn: ['^build', 'tsdown-js'] }],
579
- ]);
572
+ const resolvedTargetsByProject = new Map([['native', { targets: new Set(['build', 'tsdown-js']) }]]);
580
573
 
581
574
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
582
575
 
@@ -596,7 +589,7 @@ describe('workspace package script policy', () => {
596
589
  }
597
590
  });
598
591
 
599
- it('removes noop aggregate build targets only when they match resolved Nx plugin output', async () => {
592
+ it('removes noop aggregate build targets whose dependencies inference already produces', async () => {
600
593
  const root = await createWorkspace({
601
594
  rootName: '@smoothbricks/codebase',
602
595
  packages: [
@@ -613,12 +606,7 @@ describe('workspace package script policy', () => {
613
606
  ],
614
607
  });
615
608
  try {
616
- const resolvedTargetsByProject = new Map([
617
- [
618
- 'native',
619
- { targets: new Set(['build', 'tsc-js', 'tsdown-js']), buildDependsOn: ['^build', 'tsc-js', 'tsdown-js'] },
620
- ],
621
- ]);
609
+ const resolvedTargetsByProject = new Map([['native', { targets: new Set(['build', 'tsc-js', 'tsdown-js']) }]]);
622
610
 
623
611
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
624
612
 
@@ -629,6 +617,51 @@ describe('workspace package script policy', () => {
629
617
  }
630
618
  });
631
619
 
620
+ /**
621
+ * The AxE regression: `containium` declares its build aggregate because
622
+ * inference cannot reach the producers it needs — its own `build-cli`, and
623
+ * sibling targets that are not named `build`, so `^build` never visits them,
624
+ * and are outside the `*-js|*-web|…` output family, so the inferred aggregate
625
+ * never collects them either. Nx replaces a named property of an inferred
626
+ * target with the declared one, so the resolved graph echoes the declaration
627
+ * straight back; comparing a declaration against that echo says "redundant"
628
+ * about every hand-written aggregate and deletes the only thing that ordered
629
+ * the build. `tsc-js` is in the list on purpose: one inferable entry beside
630
+ * two that are not must still keep the whole block.
631
+ */
632
+ it('keeps noop build aggregates that name edges inference cannot produce', async () => {
633
+ const declaredBuild = {
634
+ executor: 'nx:noop',
635
+ cache: true,
636
+ dependsOn: ['build-cli', 'containium-bun:runtime', 'tsc-js'],
637
+ };
638
+ const root = await createWorkspace({
639
+ rootName: '@smoothbricks/codebase',
640
+ packages: [
641
+ { dir: 'containium-bun', name: '@axe.sc/containium-bun', nx: { name: 'containium-bun' } },
642
+ {
643
+ dir: 'containium',
644
+ name: '@axe.sc/containium',
645
+ nx: { name: 'containium', targets: { build: declaredBuild } },
646
+ },
647
+ ],
648
+ });
649
+ try {
650
+ const resolvedTargetsByProject = new Map([
651
+ ['containium-bun', { targets: new Set(['build', 'runtime', 'tsc-js']) }],
652
+ ['containium', { targets: new Set(['build', 'build-cli', 'tsc-js']) }],
653
+ ]);
654
+
655
+ applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
656
+
657
+ const containium = await readJson(join(root, 'packages/containium/package.json'));
658
+ expect(containium.nx).toEqual({ name: 'containium', targets: { build: declaredBuild } });
659
+ expect(validateWorkspaceDependencies(root, { resolvedTargetsByProject })).toBe(0);
660
+ } finally {
661
+ await rm(root, { recursive: true, force: true });
662
+ }
663
+ });
664
+
632
665
  it('accepts wildcard aggregate build dependencies', async () => {
633
666
  const root = await createWorkspace({
634
667
  rootName: '@smoothbricks/codebase',
@@ -669,9 +702,7 @@ describe('workspace package script policy', () => {
669
702
  ],
670
703
  });
671
704
  try {
672
- const resolvedTargetsByProject = new Map([
673
- ['native', { targets: new Set(['build', 'tsc-js', 'tsdown-js']), buildDependsOn: buildOutputDependencies }],
674
- ]);
705
+ const resolvedTargetsByProject = new Map([['native', { targets: new Set(['build', 'tsc-js', 'tsdown-js']) }]]);
675
706
 
676
707
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
677
708
 
@@ -2,7 +2,11 @@ import { chmodSync, existsSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { printCommandOutput, runResult, runStatus } from '../../lib/run.js';
4
4
  import { type ProjectTargets, readProjectTargets } from '../../nx/index.js';
5
- import { applyCargoFeatureUnification, validateCargoCachePolicy } from '../cargo-policy.js';
5
+ import {
6
+ applyCargoFeatureUnification,
7
+ validateCargoCachePolicy,
8
+ validateCargoToolchainInputs,
9
+ } from '../cargo-policy.js';
6
10
  import { validateGoToolchainAgreement } from '../go-toolchain.js';
7
11
  import { syncBunLockfileVersions, validateBunLockfileVersions } from '../lockfile.js';
8
12
  import { validateDevenvModuleImport, warnOnManagedFileDrift } from '../managed-files.js';
@@ -140,6 +144,11 @@ const packs: MonorepoPack[] = [
140
144
  validatePreBuild(ctx) {
141
145
  return validateCargoCachePolicy(ctx.root);
142
146
  },
147
+ // Post-build: the toolchain identity is read off the RESOLVED graph, which
148
+ // means running the inference plugin, which means its dist has to exist.
149
+ async validatePostBuild(ctx) {
150
+ return validateCargoToolchainInputs(ctx.root, await readProjectTargets(ctx.root));
151
+ },
143
152
  },
144
153
  {
145
154
  name: 'publishing',
@@ -457,7 +466,6 @@ export function resolvedTargetsByProject(projects: ProjectTargets[]): Map<string
457
466
  {
458
467
  ...(project.root ? { root: project.root } : {}),
459
468
  targets: new Set(project.targets),
460
- ...(project.buildDependsOn ? { buildDependsOn: project.buildDependsOn } : {}),
461
469
  ...(project.targetDependencies ? { targetDependencies: project.targetDependencies } : {}),
462
470
  ...(project.targetExecutors ? { targetExecutors: project.targetExecutors } : {}),
463
471
  ...(project.targetOptions ? { targetOptions: project.targetOptions } : {}),