@smoothbricks/cli 0.10.2 → 0.10.4

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 (133) hide show
  1. package/README.md +31 -23
  2. package/dist/bun/preload.d.ts +13 -0
  3. package/dist/bun/preload.d.ts.map +1 -0
  4. package/dist/bun/preload.js +12 -0
  5. package/dist/cli.js +36 -4
  6. package/dist/generate/index.d.ts.map +1 -1
  7. package/dist/generate/index.js +0 -6
  8. package/dist/github-ci/index.d.ts +17 -2
  9. package/dist/github-ci/index.d.ts.map +1 -1
  10. package/dist/github-ci/index.js +192 -26
  11. package/dist/github-ci/outputs.d.ts +22 -0
  12. package/dist/github-ci/outputs.d.ts.map +1 -0
  13. package/dist/github-ci/outputs.js +648 -0
  14. package/dist/lib/cli-package.js +2 -2
  15. package/dist/lib/json.d.ts +118 -11
  16. package/dist/lib/json.d.ts.map +1 -1
  17. package/dist/lib/json.js +317 -29
  18. package/dist/lib/workspace.d.ts +7 -14
  19. package/dist/lib/workspace.d.ts.map +1 -1
  20. package/dist/lib/workspace.js +76 -82
  21. package/dist/monorepo/ci-workflow.d.ts +2 -0
  22. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  23. package/dist/monorepo/ci-workflow.js +28 -2
  24. package/dist/monorepo/git-config.js +2 -2
  25. package/dist/monorepo/index.d.ts +3 -1
  26. package/dist/monorepo/index.d.ts.map +1 -1
  27. package/dist/monorepo/index.js +6 -2
  28. package/dist/monorepo/lockfile.d.ts +13 -1
  29. package/dist/monorepo/lockfile.d.ts.map +1 -1
  30. package/dist/monorepo/lockfile.js +33 -21
  31. package/dist/monorepo/managed-files.d.ts +12 -0
  32. package/dist/monorepo/managed-files.d.ts.map +1 -1
  33. package/dist/monorepo/managed-files.js +86 -28
  34. package/dist/monorepo/package-policy.d.ts.map +1 -1
  35. package/dist/monorepo/package-policy.js +78 -45
  36. package/dist/monorepo/packed-manifest.d.ts +5 -2
  37. package/dist/monorepo/packed-manifest.d.ts.map +1 -1
  38. package/dist/monorepo/packed-manifest.js +16 -47
  39. package/dist/monorepo/packed-package.d.ts.map +1 -1
  40. package/dist/monorepo/packed-package.js +28 -16
  41. package/dist/monorepo/packs/index.d.ts +3 -0
  42. package/dist/monorepo/packs/index.d.ts.map +1 -1
  43. package/dist/monorepo/packs/index.js +18 -6
  44. package/dist/monorepo/publish-workflow.d.ts +1 -0
  45. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  46. package/dist/monorepo/publish-workflow.js +242 -3
  47. package/dist/monorepo/runtime.d.ts +15 -0
  48. package/dist/monorepo/runtime.d.ts.map +1 -1
  49. package/dist/monorepo/runtime.js +66 -12
  50. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  51. package/dist/monorepo/tool-validation.js +32 -23
  52. package/dist/monorepo/wrangler.d.ts.map +1 -1
  53. package/dist/monorepo/wrangler.js +13 -13
  54. package/dist/nx/index.d.ts +12 -4
  55. package/dist/nx/index.d.ts.map +1 -1
  56. package/dist/nx/index.js +86 -19
  57. package/dist/pr/index.d.ts.map +1 -1
  58. package/dist/pr/index.js +28 -32
  59. package/dist/release/candidates.d.ts +3 -2
  60. package/dist/release/candidates.d.ts.map +1 -1
  61. package/dist/release/candidates.js +4 -5
  62. package/dist/release/github-release.d.ts +3 -0
  63. package/dist/release/github-release.d.ts.map +1 -1
  64. package/dist/release/github-release.js +11 -0
  65. package/dist/release/index.d.ts +20 -1
  66. package/dist/release/index.d.ts.map +1 -1
  67. package/dist/release/index.js +221 -89
  68. package/dist/release/orchestration.d.ts +8 -1
  69. package/dist/release/orchestration.d.ts.map +1 -1
  70. package/dist/release/orchestration.js +38 -1
  71. package/dist/wrangler/prepare-env.d.ts +0 -1
  72. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  73. package/dist/wrangler/prepare-env.js +100 -51
  74. package/dist/wrangler/scaffold.d.ts.map +1 -1
  75. package/dist/wrangler/scaffold.js +12 -12
  76. package/managed/raw/git-format-staged.yml +19 -1
  77. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +6 -0
  78. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  79. package/managed/raw/tooling/direnv/setup-environment.ts +187 -19
  80. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  81. package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
  82. package/managed/templates/github/actions/cache-node-modules/action.yml +6 -2
  83. package/managed/templates/github/actions/cache-nx/action.yml +2 -2
  84. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  85. package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
  86. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  87. package/managed/templates/github/workflows/managed-files.yml +80 -0
  88. package/package.json +18 -4
  89. package/src/bun/preload.ts +12 -0
  90. package/src/cli.ts +49 -11
  91. package/src/generate/index.ts +0 -6
  92. package/src/github-ci/index.test.ts +486 -0
  93. package/src/github-ci/index.ts +220 -30
  94. package/src/github-ci/outputs.ts +505 -0
  95. package/src/lib/cli-package.ts +2 -2
  96. package/src/lib/json.ts +291 -32
  97. package/src/lib/workspace.ts +88 -100
  98. package/src/monorepo/__tests__/ci-workflow.test.ts +14 -3
  99. package/src/monorepo/__tests__/publish-workflow.test.ts +222 -1
  100. package/src/monorepo/ci-workflow.ts +28 -2
  101. package/src/monorepo/git-config.ts +2 -2
  102. package/src/monorepo/index.ts +6 -2
  103. package/src/monorepo/lockfile.test.ts +35 -36
  104. package/src/monorepo/lockfile.ts +48 -24
  105. package/src/monorepo/managed-files.test.ts +63 -0
  106. package/src/monorepo/managed-files.ts +94 -34
  107. package/src/monorepo/package-policy.test.ts +70 -51
  108. package/src/monorepo/package-policy.ts +104 -59
  109. package/src/monorepo/packed-manifest.ts +18 -52
  110. package/src/monorepo/packed-package.ts +43 -22
  111. package/src/monorepo/packs/index.test.ts +18 -1
  112. package/src/monorepo/packs/index.ts +20 -7
  113. package/src/monorepo/publish-workflow.ts +482 -3
  114. package/src/monorepo/runtime.test.ts +42 -3
  115. package/src/monorepo/runtime.ts +90 -12
  116. package/src/monorepo/tool-validation.ts +35 -25
  117. package/src/monorepo/wrangler.test.ts +11 -15
  118. package/src/monorepo/wrangler.ts +15 -15
  119. package/src/nx/index.test.ts +43 -0
  120. package/src/nx/index.ts +110 -23
  121. package/src/pr/index.ts +42 -36
  122. package/src/release/__tests__/candidates.test.ts +5 -4
  123. package/src/release/__tests__/fixture-repo.test.ts +18 -16
  124. package/src/release/__tests__/github-release.test.ts +11 -0
  125. package/src/release/__tests__/orchestration.test.ts +57 -2
  126. package/src/release/__tests__/trust-publisher.test.ts +98 -2
  127. package/src/release/candidates.ts +10 -17
  128. package/src/release/github-release.ts +12 -0
  129. package/src/release/index.ts +321 -112
  130. package/src/release/orchestration.ts +55 -3
  131. package/src/wrangler/prepare-env.ts +75 -54
  132. package/src/wrangler/scaffold.test.ts +9 -12
  133. package/src/wrangler/scaffold.ts +13 -13
@@ -1,22 +1,18 @@
1
- import { execSync } from 'node:child_process';
2
1
  import { $ } from 'bun';
3
- import { isRecord, recordProperty } from '../lib/json.js';
2
+ import { type PackageJson, parsePackageJsonText } from '../lib/json.js';
4
3
  import { decode } from '../lib/run.js';
5
4
  import type { PackageInfo } from '../lib/workspace.js';
6
5
  import { getWorkspacePackages, workspaceDependencyFields } from '../lib/workspace.js';
6
+ import { publishPackDependencyVersion } from './lockfile.js';
7
7
 
8
- export async function readPackedPackageJson(
9
- root: string,
10
- tarball: string,
11
- packageName: string,
12
- ): Promise<Record<string, unknown>> {
8
+ export async function readPackedPackageJson(root: string, tarball: string, packageName: string): Promise<PackageJson> {
13
9
  const result = await $`tar -xOf ${tarball} package/package.json`.cwd(root).quiet().nothrow();
14
10
  if (result.exitCode !== 0) {
15
11
  throw new Error(`${packageName}: unable to inspect packed package.json.`);
16
12
  }
17
- const parsed = JSON.parse(decode(result.stdout));
18
- if (!isRecord(parsed)) {
19
- throw new Error(`${packageName}: packed package.json is not an object.`);
13
+ const parsed = parsePackageJsonText(decode(result.stdout));
14
+ if (!parsed) {
15
+ throw new Error(`${packageName}: packed package.json is invalid.`);
20
16
  }
21
17
  return parsed;
22
18
  }
@@ -24,21 +20,21 @@ export async function readPackedPackageJson(
24
20
  export function validatePackedWorkspaceDependencies(
25
21
  root: string,
26
22
  sourcePackage: PackageInfo,
27
- packedPackage: Record<string, unknown>,
23
+ packedPackage: PackageJson,
24
+ options: { mode?: 'install' | 'publish' } = {},
28
25
  ): string[] {
26
+ const mode = options.mode ?? 'publish';
29
27
  const workspacePackages = getWorkspacePackages(root);
30
- const workspaceVersions = new Map(workspacePackages.map((pkg) => [pkg.name, pkg.version]));
31
- const projectNameByPackage = new Map(workspacePackages.map((pkg) => [pkg.name, pkg.projectName]));
32
28
  const failures: string[] = [];
33
29
  for (const field of workspaceDependencyFields) {
34
- const sourceDependencies = recordProperty(sourcePackage.json, field);
35
- const packedDependencies = recordProperty(packedPackage, field);
30
+ const sourceDependencies = sourcePackage.json[field];
31
+ const packedDependencies = packedPackage[field];
36
32
  if (!sourceDependencies && !packedDependencies) {
37
33
  continue;
38
34
  }
39
35
 
40
36
  for (const [name, range] of Object.entries(packedDependencies ?? {})) {
41
- if (typeof range === 'string' && range.startsWith('workspace:')) {
37
+ if (range.startsWith('workspace:')) {
42
38
  failures.push(`${sourcePackage.path}: packed ${field}.${name} must not contain ${range}`);
43
39
  }
44
40
  }
@@ -47,54 +43,24 @@ export function validatePackedWorkspaceDependencies(
47
43
  continue;
48
44
  }
49
45
  for (const [name, sourceRange] of Object.entries(sourceDependencies)) {
50
- const workspaceVersion = workspaceVersions.get(name);
51
- if (!workspaceVersion) {
46
+ const dep = workspacePackages.find((pkg) => pkg.name === name);
47
+ if (!dep) {
52
48
  continue;
53
49
  }
54
50
  const packedRange = packedDependencies?.[name];
55
51
  if (sourceRange !== 'workspace:*') {
56
52
  failures.push(`${sourcePackage.path}: source ${field}.${name} must use workspace:*`);
57
53
  }
58
- // When the workspace dep is at a prerelease version that was never
59
- // published, the lockfile sync rewrites it to the latest stable tag
60
- // version. Accept that version as valid.
61
- const projectName = projectNameByPackage.get(name);
54
+ // install: what day-to-day pack embeds (package.json / lockfile aligned)
55
+ // publish: what pre-publish sync embeds (unpublished -next last stable tag)
62
56
  const expectedVersion =
63
- workspaceVersion.includes('-') && projectName
64
- ? (latestStableTagVersion(root, projectName) ?? workspaceVersion)
65
- : workspaceVersion;
57
+ mode === 'publish' ? publishPackDependencyVersion(root, dep.projectName, dep.version) : dep.version;
66
58
  if (packedRange !== expectedVersion) {
67
59
  failures.push(
68
- `${sourcePackage.path}: packed ${field}.${name} must be ${expectedVersion}, got ${formatRange(packedRange)}`,
60
+ `${sourcePackage.path}: packed ${field}.${name} must be ${expectedVersion}, got ${packedRange ?? '<missing>'}`,
69
61
  );
70
62
  }
71
63
  }
72
64
  }
73
65
  return failures;
74
66
  }
75
-
76
- function formatRange(value: unknown): string {
77
- return typeof value === 'string' ? value : '<missing>';
78
- }
79
-
80
- function latestStableTagVersion(root: string, projectName: string): string | null {
81
- try {
82
- const output = execSync(`git tag --list '${projectName}@*' --sort=-v:refname`, {
83
- cwd: root,
84
- encoding: 'utf8',
85
- stdio: ['pipe', 'pipe', 'pipe'],
86
- });
87
- const prefix = `${projectName}@`;
88
- for (const line of output.split('\n')) {
89
- const tag = line.trim();
90
- if (!tag.startsWith(prefix)) continue;
91
- const version = tag.slice(prefix.length);
92
- if (version && !version.includes('-')) {
93
- return version;
94
- }
95
- }
96
- return null;
97
- } catch {
98
- return null;
99
- }
100
- }
@@ -4,7 +4,9 @@ import { dirname, join, relative } from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { publint } from 'publint';
6
6
  import { formatMessage } from 'publint/utils';
7
- import { isRecord } from '../lib/json.js';
7
+ import typia from 'typia';
8
+ import type { PackageExports, PackageJson } from '../lib/json.js';
9
+ import { parsePackageJsonText } from '../lib/json.js';
8
10
  import { printCommandOutput, runResult } from '../lib/run.js';
9
11
  import type { PackageInfo } from '../lib/workspace.js';
10
12
  import { listPublicPackages } from '../lib/workspace.js';
@@ -80,7 +82,7 @@ async function validatePackedManifest(
80
82
  ): Promise<number> {
81
83
  let failures = 0;
82
84
  const packedPackage = await readPackedPackageJson(root, packed.path, pkg.name);
83
- for (const message of validatePackedWorkspaceDependencies(root, pkg, packedPackage)) {
85
+ for (const message of validatePackedWorkspaceDependencies(root, pkg, packedPackage, { mode: 'install' })) {
84
86
  console.error(message);
85
87
  failures++;
86
88
  }
@@ -92,11 +94,10 @@ async function validateAttw(root: string, pkg: PackageInfo, packed: { path: stri
92
94
  const analysis = await attw.checkPackage(await createAttwPackageFromTarball(attw, root, packed.path), {
93
95
  excludeEntrypoints: nonJsExportEntrypoints(pkg.json.exports),
94
96
  });
95
- if (!isRecord(analysis) || analysis.types === false) {
97
+ if (!isAttwAnalysis(analysis) || analysis.types === false) {
96
98
  return 0;
97
99
  }
98
- const rawProblems = Array.isArray(analysis.problems) ? analysis.problems : [];
99
- const problems = rawProblems.filter(isReportedAttwProblem);
100
+ const problems = (analysis.problems ?? []).filter(isReportedAttwProblem);
100
101
  if (problems.length === 0) {
101
102
  return 0;
102
103
  }
@@ -112,10 +113,30 @@ interface AttwCore {
112
113
  checkPackage: (pkg: unknown, options: { excludeEntrypoints: string[] }) => Promise<unknown>;
113
114
  }
114
115
 
116
+ interface AttwAnalysis {
117
+ types?: unknown;
118
+ problems?: unknown[];
119
+ }
120
+
121
+ interface AttwProblem {
122
+ kind: string;
123
+ resolutionKind?: string;
124
+ entrypoint?: string;
125
+ }
126
+
127
+ interface AttwCoreExport {
128
+ Package?: unknown;
129
+ checkPackage?: unknown;
130
+ }
131
+
132
+ const isAttwAnalysis = typia.createIs<AttwAnalysis>();
133
+ const isAttwProblem = typia.createIs<AttwProblem>();
134
+ const isAttwCoreExport = typia.createIs<AttwCoreExport>();
135
+
115
136
  async function loadAttwCore(): Promise<AttwCore> {
116
137
  const packageJson = fileURLToPath(import.meta.resolve('@arethetypeswrong/core/package.json'));
117
- const core = await import(pathToFileURL(join(dirname(packageJson), 'dist', 'index.js')).href);
118
- if (!isRecord(core)) {
138
+ const core: unknown = await import(pathToFileURL(join(dirname(packageJson), 'dist', 'index.js')).href);
139
+ if (!isAttwCoreExport(core)) {
119
140
  throw new Error('@arethetypeswrong/core does not expose the expected API');
120
141
  }
121
142
  const PackageConstructor = core.Package;
@@ -146,8 +167,8 @@ async function createAttwPackageFromTarball(attw: AttwCore, root: string, tarbal
146
167
 
147
168
  function createAttwPackageFromDirectory(attw: AttwCore, packageDir: string): unknown {
148
169
  const packageJson = readJsonFile(join(packageDir, 'package.json'));
149
- const packageName = typeof packageJson.name === 'string' ? packageJson.name : null;
150
- const packageVersion = typeof packageJson.version === 'string' ? packageJson.version : null;
170
+ const packageName = packageJson.name;
171
+ const packageVersion = packageJson.version;
151
172
  if (!packageName || !packageVersion) {
152
173
  throw new Error('packed package.json must contain name and version');
153
174
  }
@@ -172,16 +193,16 @@ function collectAttwFiles(root: string, current: string, packageName: string, fi
172
193
  }
173
194
  }
174
195
 
175
- function readJsonFile(path: string): Record<string, unknown> {
176
- const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
177
- if (!isRecord(parsed)) {
196
+ function readJsonFile(path: string): PackageJson {
197
+ const parsed = parsePackageJsonText(readFileSync(path, 'utf8'));
198
+ if (!parsed) {
178
199
  throw new Error(`${path} is not a JSON object`);
179
200
  }
180
201
  return parsed;
181
202
  }
182
203
 
183
- function isReportedAttwProblem(problem: unknown): boolean {
184
- if (!isRecord(problem) || typeof problem.kind !== 'string') {
204
+ function isReportedAttwProblem(problem: unknown): problem is AttwProblem {
205
+ if (!isAttwProblem(problem)) {
185
206
  return false;
186
207
  }
187
208
  const flag = attwProblemFlag(problem.kind);
@@ -222,10 +243,7 @@ function attwProblemFlag(kind: string): string | null {
222
243
  }
223
244
  }
224
245
 
225
- function formatProblemLocation(problem: unknown): string {
226
- if (!isRecord(problem)) {
227
- return '';
228
- }
246
+ function formatProblemLocation(problem: AttwProblem): string {
229
247
  const entrypoint = typeof problem.entrypoint === 'string' ? ` ${problem.entrypoint}` : '';
230
248
  const resolution =
231
249
  typeof problem.resolutionKind === 'string' ? ` ${formatResolutionKind(problem.resolutionKind)}` : '';
@@ -242,8 +260,8 @@ function formatResolutionKind(kind: string): string {
242
260
  return kind;
243
261
  }
244
262
 
245
- function nonJsExportEntrypoints(exports: unknown): string[] {
246
- if (!isRecord(exports)) {
263
+ function nonJsExportEntrypoints(exports: PackageExports): string[] {
264
+ if (exports === null || exports === undefined || typeof exports === 'string') {
247
265
  return [];
248
266
  }
249
267
  return Object.entries(exports)
@@ -251,13 +269,16 @@ function nonJsExportEntrypoints(exports: unknown): string[] {
251
269
  .map(([key]) => key);
252
270
  }
253
271
 
254
- function exportPointsToNonJs(value: unknown): boolean {
272
+ function exportPointsToNonJs(value: PackageExports): boolean {
255
273
  if (typeof value === 'string') {
256
274
  // attw resolves every entrypoint as a module; assets can never have types.
257
275
  // Existence of the target files is still validated by publint.
258
276
  return value.endsWith('.wasm') || value.endsWith('.css');
259
277
  }
260
- return isRecord(value) && Object.values(value).some(exportPointsToNonJs);
278
+ if (value === null || value === undefined) {
279
+ return false;
280
+ }
281
+ return Object.values(value).some(exportPointsToNonJs);
261
282
  }
262
283
 
263
284
  async function packPackage(root: string, pkg: PackageInfo): Promise<{ path: string; arrayBuffer: ArrayBuffer }> {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
2
  import type { MonorepoPack } from './index.js';
3
- import { runValidatePacks } from './index.js';
3
+ import { resolvedTargetsByProject, runValidatePacks } from './index.js';
4
4
 
5
5
  const ctx = { root: '/workspace', syncRuntime: false };
6
6
 
@@ -186,4 +186,21 @@ describe('monorepo validation pack phases', () => {
186
186
  expect(result).toEqual({ failures: 1, failedChecks: 1 });
187
187
  expect(events).toEqual(['pre-fix', 'build']);
188
188
  });
189
+
190
+ it('propagates parsed target dependencies through the production adapter', () => {
191
+ const targetDependencies = new Map([
192
+ ['build', ['compile-linux']],
193
+ ['compile-linux', ['package-linux']],
194
+ ]);
195
+
196
+ const resolved = resolvedTargetsByProject([
197
+ {
198
+ project: 'native',
199
+ targets: ['build', 'compile-linux', 'package-linux'],
200
+ targetDependencies,
201
+ },
202
+ ]);
203
+
204
+ expect(resolved.get('native')?.targetDependencies).toBe(targetDependencies);
205
+ });
189
206
  });
@@ -1,9 +1,9 @@
1
1
  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
- import { readProjectTargets } from '../../nx/index.js';
4
+ import { type ProjectTargets, readProjectTargets } from '../../nx/index.js';
5
5
  import { syncBunLockfileVersions, validateBunLockfileVersions } from '../lockfile.js';
6
- import { validateManagedFiles } from '../managed-files.js';
6
+ import { warnOnManagedFileDrift } from '../managed-files.js';
7
7
  import { fixNxSync, validateNxSync } from '../nx-sync.js';
8
8
  import { fixPackageHygiene, validatePackageHygiene } from '../package-hygiene.js';
9
9
  import {
@@ -24,7 +24,7 @@ import {
24
24
  validatePackedPublicPackagePublint,
25
25
  validatePackedPublicPackageTypes,
26
26
  } from '../packed-package.js';
27
- import { syncRootRuntimeVersions } from '../runtime.js';
27
+ import { syncRootRuntimeVersions, validateRootRuntimeVersions } from '../runtime.js';
28
28
  import { applyToolConfigDefaults, validateToolConfig } from '../tool-validation.js';
29
29
  import { applyWranglerDefaults, validateWrangler } from '../wrangler.js';
30
30
 
@@ -79,13 +79,22 @@ const packs: MonorepoPack[] = [
79
79
  async fixPreBuild(ctx) {
80
80
  applyFixableMonorepoDefaults(ctx.root);
81
81
  await applyToolConfigDefaults(ctx.root);
82
- syncBunLockfileVersions(ctx.root);
82
+ // Install/CI alignment only — do not rewrite -next to stable tags here.
83
+ syncBunLockfileVersions(ctx.root, { mode: 'install' });
83
84
  await fixNxSync(ctx.root, ctx.verbose === true);
84
85
  applyWorkspaceDependencyDefaults(ctx.root, { resolvedTargetsByProject: await readResolvedTargetsByProject(ctx) });
85
86
  },
86
87
  async validatePreBuild(ctx) {
88
+ // Runtime pins validate against the live PATH runtimes (devenv shell),
89
+ // never a stored template — outside devenv the PATH is not authoritative,
90
+ // mirroring the init-time syncRuntime gate above.
91
+ const runtimeFailures = ctx.syncRuntime ? await validateRootRuntimeVersions(ctx.root) : 0;
92
+ // Managed-file drift is derived state (CLI template x pinned version) with
93
+ // its own remediation flow; it warns instead of failing so validation only
94
+ // blocks on actual package issues. See warnOnManagedFileDrift.
95
+ warnOnManagedFileDrift(ctx.root);
87
96
  return (
88
- validateManagedFiles(ctx.root) +
97
+ runtimeFailures +
89
98
  validateRootPackagePolicy(ctx.root) +
90
99
  validateToolConfig(ctx.root) +
91
100
  validateNxProjectNames(ctx.root) +
@@ -400,14 +409,14 @@ function printCapturedOutput(output: CapturedOutput): void {
400
409
  }
401
410
  }
402
411
 
403
- async function readResolvedTargetsByProject(ctx: MonorepoContext): Promise<Map<string, ResolvedProjectTargets>> {
404
- const projects = await readProjectTargets(ctx.root);
412
+ export function resolvedTargetsByProject(projects: ProjectTargets[]): Map<string, ResolvedProjectTargets> {
405
413
  return new Map(
406
414
  projects.map((project) => [
407
415
  project.project,
408
416
  {
409
417
  targets: new Set(project.targets),
410
418
  ...(project.buildDependsOn ? { buildDependsOn: project.buildDependsOn } : {}),
419
+ ...(project.targetDependencies ? { targetDependencies: project.targetDependencies } : {}),
411
420
  ...(project.targetExecutors ? { targetExecutors: project.targetExecutors } : {}),
412
421
  ...(project.targetScripts ? { targetScripts: project.targetScripts } : {}),
413
422
  },
@@ -415,6 +424,10 @@ async function readResolvedTargetsByProject(ctx: MonorepoContext): Promise<Map<s
415
424
  );
416
425
  }
417
426
 
427
+ async function readResolvedTargetsByProject(ctx: MonorepoContext): Promise<Map<string, ResolvedProjectTargets>> {
428
+ return resolvedTargetsByProject(await readProjectTargets(ctx.root));
429
+ }
430
+
418
431
  function ensureLocalSmooShim(root: string): void {
419
432
  const shim = join(root, 'tooling', 'smoo');
420
433
  if (!existsSync(shim)) {