@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.
- package/README.md +68 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +28 -4
- package/dist/lib/json.d.ts +22 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +15 -3
- package/dist/monorepo/cargo-policy.d.ts +17 -0
- package/dist/monorepo/cargo-policy.d.ts.map +1 -1
- package/dist/monorepo/cargo-policy.js +73 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +4 -2
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +6 -2
- package/dist/nx/index.d.ts +6 -1
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +14 -1
- package/dist/secrets/commands.d.ts +9 -5
- package/dist/secrets/commands.d.ts.map +1 -1
- package/dist/secrets/commands.js +122 -49
- package/dist/secrets/index.d.ts +42 -56
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +52 -79
- package/dist/secrets/resolver.d.ts +59 -0
- package/dist/secrets/resolver.d.ts.map +1 -0
- package/dist/secrets/resolver.js +100 -0
- package/dist/secrets/run.d.ts +23 -0
- package/dist/secrets/run.d.ts.map +1 -0
- package/dist/secrets/run.js +130 -0
- package/dist/secrets/status.d.ts +177 -0
- package/dist/secrets/status.d.ts.map +1 -0
- package/dist/secrets/status.js +724 -0
- package/dist/wrangler/deploy-stage.d.ts +1 -1
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +22 -20
- package/dist/wrangler/deployed-version.d.ts.map +1 -1
- package/dist/wrangler/deployed-version.js +7 -12
- package/dist/wrangler/flat-config.d.ts +1 -4
- package/dist/wrangler/flat-config.d.ts.map +1 -1
- package/dist/wrangler/flat-config.js +98 -44
- package/dist/wrangler/prepare-env.d.ts +4 -3
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +7 -5
- package/dist/wrangler/source-config.d.ts +24 -0
- package/dist/wrangler/source-config.d.ts.map +1 -0
- package/dist/wrangler/source-config.js +110 -0
- package/dist/wrangler/stage.d.ts +55 -23
- package/dist/wrangler/stage.d.ts.map +1 -1
- package/dist/wrangler/stage.js +81 -162
- package/managed/raw/tooling/direnv/devenv.smoo.nix +19 -3
- package/managed/raw/tooling/direnv/secret-references.ts +280 -76
- package/managed/raw/tooling/direnv/setup-environment.ts +58 -2
- package/managed/raw/tsconfig.lib.json +28 -0
- package/package.json +7 -2
- package/src/cli.ts +34 -5
- package/src/lib/json.ts +23 -1
- package/src/monorepo/cargo-policy.test.ts +91 -1
- package/src/monorepo/cargo-policy.ts +87 -1
- package/src/monorepo/index.ts +8 -2
- package/src/monorepo/package-policy.test.ts +50 -19
- package/src/monorepo/packs/index.ts +10 -2
- package/src/monorepo/secret-references.test.ts +412 -6
- package/src/monorepo/setup-environment.test.ts +181 -0
- package/src/nx/index.test.ts +8 -2
- package/src/nx/index.ts +20 -2
- package/src/secrets/commands.test.ts +186 -4
- package/src/secrets/commands.ts +142 -51
- package/src/secrets/index.test.ts +4 -10
- package/src/secrets/index.ts +54 -115
- package/src/secrets/resolver.ts +130 -0
- package/src/secrets/run.test.ts +359 -0
- package/src/secrets/run.ts +148 -0
- package/src/secrets/status.test.ts +164 -0
- package/src/secrets/status.ts +297 -0
- package/src/wrangler/deploy-stage.test.ts +95 -2
- package/src/wrangler/deploy-stage.ts +26 -23
- package/src/wrangler/deployed-version.ts +7 -11
- package/src/wrangler/flat-config.test.ts +9 -4
- package/src/wrangler/flat-config.ts +1 -20
- package/src/wrangler/format-parity.test.ts +433 -0
- package/src/wrangler/prepare-env.ts +7 -5
- package/src/wrangler/source-config.test.ts +102 -0
- package/src/wrangler/source-config.ts +110 -0
- package/src/wrangler/stage.test.ts +61 -32
- 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
|
|
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
|
-
.
|
|
547
|
-
|
|
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>',
|
|
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
|
-
/**
|
|
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
|
|
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
|
package/src/monorepo/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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 {
|
|
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 } : {}),
|