@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,7 +1,31 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { $ } from 'bun';
4
- import { getOrCreateRecord, readJsonObject, setStringProperty, writeJsonObject } from '../lib/json.js';
4
+ import {
5
+ ensureDependencyMap,
6
+ ensureEngines,
7
+ type PackageJson,
8
+ parseStringArrayText,
9
+ readJsonObject,
10
+ setPackageStringField,
11
+ setStringProperty,
12
+ writeJsonObject,
13
+ } from '../lib/json.js';
14
+
15
+ export interface RuntimeVersions {
16
+ node: string;
17
+ bun: string;
18
+ }
19
+
20
+ /** Live runtime versions from the shell PATH — the single source sync AND validate derive from. */
21
+ export async function runtimeVersionsFromPath(root: string): Promise<RuntimeVersions> {
22
+ const node = runtimeCommand(root, 'node');
23
+ const bun = runtimeCommand(root, 'bun');
24
+ return {
25
+ node: (await $`${node} --version`.cwd(root).text()).trim().replace(/^v/, ''),
26
+ bun: (await $`${bun} --version`.cwd(root).text()).trim(),
27
+ };
28
+ }
5
29
 
6
30
  export async function syncRootRuntimeVersions(root: string): Promise<void> {
7
31
  const packageJsonPath = join(root, 'package.json');
@@ -9,20 +33,17 @@ export async function syncRootRuntimeVersions(root: string): Promise<void> {
9
33
  if (!packageJson) {
10
34
  throw new Error('package.json not found or invalid');
11
35
  }
12
- const node = runtimeCommand(root, 'node');
13
- const bun = runtimeCommand(root, 'bun');
14
- const nodeVersion = (await $`${node} --version`.cwd(root).text()).trim().replace(/^v/, '');
15
- const bunVersion = (await $`${bun} --version`.cwd(root).text()).trim();
36
+ const { node: nodeVersion, bun: bunVersion } = await runtimeVersionsFromPath(root);
16
37
  const nodeMajor = nodeVersion.split('.', 1)[0];
17
38
  if (!nodeMajor) {
18
39
  throw new Error(`Unable to derive Node major version from ${nodeVersion}`);
19
40
  }
20
41
 
21
42
  let changed = false;
22
- const engines = getOrCreateRecord(packageJson, 'engines');
43
+ const engines = ensureEngines(packageJson);
23
44
  changed = setStringProperty(engines, 'node', `>=${nodeMajor}.0.0`) || changed;
24
- changed = setStringProperty(packageJson, 'packageManager', `bun@${bunVersion}`) || changed;
25
- const devDependencies = getOrCreateRecord(packageJson, 'devDependencies');
45
+ changed = setPackageStringField(packageJson, 'packageManager', `bun@${bunVersion}`) || changed;
46
+ const devDependencies = ensureDependencyMap(packageJson, 'devDependencies');
26
47
  changed =
27
48
  setStringProperty(
28
49
  devDependencies,
@@ -42,6 +63,58 @@ export async function syncRootRuntimeVersions(root: string): Promise<void> {
42
63
  }
43
64
  }
44
65
 
66
+ /**
67
+ * Validate that package.json runtime pins agree with the LIVE PATH runtimes,
68
+ * never a stored template. Offline by design: structural alignment only
69
+ * (majors, exact bun pin, no `~major.0.0` floor) — the registry is not
70
+ * consulted. Repair: `smoo monorepo init --runtime-only` inside the devenv shell.
71
+ */
72
+ export async function validateRootRuntimeVersions(root: string): Promise<number> {
73
+ const packageJson = readJsonObject(join(root, 'package.json'));
74
+ if (!packageJson) {
75
+ console.error('package.json not found or invalid');
76
+ return 1;
77
+ }
78
+ return validateRuntimePins(packageJson, await runtimeVersionsFromPath(root));
79
+ }
80
+
81
+ export function validateRuntimePins(packageJson: PackageJson, runtime: RuntimeVersions): number {
82
+ const nodeMajor = runtime.node.split('.', 1)[0];
83
+ if (!nodeMajor) {
84
+ throw new Error(`Unable to derive Node major version from ${runtime.node}`);
85
+ }
86
+ let failures = 0;
87
+ const repair = 'run `smoo monorepo init --runtime-only` inside the devenv shell';
88
+ const enginesNode = packageJson.engines?.node ?? null;
89
+ if (enginesNode !== `>=${nodeMajor}.0.0`) {
90
+ console.error(
91
+ `package.json engines.node is ${enginesNode ?? 'missing'} but the PATH node is v${runtime.node} — expected >=${nodeMajor}.0.0; ${repair}`,
92
+ );
93
+ failures++;
94
+ }
95
+ const packageManager = packageJson.packageManager ?? null;
96
+ if (packageManager !== `bun@${runtime.bun}`) {
97
+ console.error(
98
+ `package.json packageManager is ${packageManager ?? 'missing'} but the PATH bun is ${runtime.bun} — expected bun@${runtime.bun}; ${repair}`,
99
+ );
100
+ failures++;
101
+ }
102
+ const typesNode = packageJson.devDependencies?.['@types/node'] ?? null;
103
+ const typesNodeParts = typesNode ? /^~(\d+)\.(\d+)\.(\d+)$/.exec(typesNode) : null;
104
+ if (!typesNodeParts || typesNodeParts[1] !== nodeMajor) {
105
+ console.error(
106
+ `package.json @types/node is ${typesNode ?? 'missing'} but the PATH node is v${runtime.node} — types track the runtime major (~${nodeMajor}.x.y, newest published minor); ${repair}`,
107
+ );
108
+ failures++;
109
+ } else if (typesNodeParts[2] === '0' && typesNodeParts[3] === '0') {
110
+ console.error(
111
+ `package.json @types/node is pinned to the ~${nodeMajor}.0.0 floor — tilde locks the first patch line and strands the repo on early broken releases; ${repair} to repin to the newest published ${nodeMajor}.x`,
112
+ );
113
+ failures++;
114
+ }
115
+ return failures;
116
+ }
117
+
45
118
  type RuntimeTypesPinMode = 'major' | 'exact';
46
119
 
47
120
  async function runtimeTypesRange(
@@ -51,8 +124,8 @@ async function runtimeTypesRange(
51
124
  pinMode: RuntimeTypesPinMode,
52
125
  ): Promise<string> {
53
126
  const versionsText = await $`bun pm view ${packageName} versions --json`.cwd(root).text();
54
- const versions = JSON.parse(versionsText) as unknown;
55
- if (!Array.isArray(versions) || !versions.every((version) => typeof version === 'string')) {
127
+ const versions = parseStringArrayText(versionsText);
128
+ if (!versions) {
56
129
  throw new Error(`Unable to read published ${packageName} versions`);
57
130
  }
58
131
  return runtimeTypesRangeForPublishedVersions(packageName, runtimeVersion, pinMode, versions);
@@ -71,8 +144,13 @@ export function runtimeTypesRangeForPublishedVersions(
71
144
 
72
145
  if (pinMode === 'major') {
73
146
  const runtimeMajor = parsedRuntimeVersion[0].toString();
74
- if (latestVersion(versions.filter((version) => versionMajor(version) === runtimeMajor))) {
75
- return `~${runtimeMajor}.0.0`;
147
+ // Newest published types WITHIN the runtime major — never the `~major.0.0`
148
+ // floor: tilde locks the patch line, so the floor can strand consumers on a
149
+ // broken early release (e.g. @types/node 24.0.x's URLPattern/DOM TS2403
150
+ // conflict, fixed in 24.13) with no path to the repaired minors.
151
+ const latestInMajor = latestVersion(versions.filter((version) => versionMajor(version) === runtimeMajor));
152
+ if (latestInMajor) {
153
+ return `~${latestInMajor}`;
76
154
  }
77
155
  } else if (versions.includes(runtimeVersion)) {
78
156
  return runtimeVersion;
@@ -1,7 +1,14 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
+ import typia from 'typia';
3
4
  import { cliPackageVersion, isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
4
- import { getOrCreateRecord, readJsonObject, recordProperty, setStringProperty, writeJsonObject } from '../lib/json.js';
5
+ import {
6
+ ensureDependencyMap,
7
+ readJsonObject,
8
+ setPackageStringField,
9
+ setStringProperty,
10
+ writeJsonObject,
11
+ } from '../lib/json.js';
5
12
  import { type PackageJson, readPackageJsonObject } from '../lib/workspace.js';
6
13
 
7
14
  interface RequiredDependency {
@@ -23,6 +30,11 @@ export interface ToolContext {
23
30
  policy: ToolPolicy;
24
31
  }
25
32
 
33
+ interface RegistryPackument {
34
+ versions: Record<string, unknown>;
35
+ }
36
+
37
+ const isRegistryPackument = typia.createIs<RegistryPackument>();
26
38
  const rootDevDependencies: RequiredDependency[] = [
27
39
  { name: '@biomejs/biome', fallbackVersion: '^2.3.5', minimumVersion: '2.3.0', prefix: '^' },
28
40
  { name: '@nx/js', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
@@ -44,7 +56,7 @@ const rootDevDependencies: RequiredDependency[] = [
44
56
  { name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
45
57
  { name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', prefix: '^' },
46
58
  { name: 'ttsc', fallbackVersion: '^0.18.4', minimumVersion: '0.18.4', prefix: '^' },
47
- // Nx and typescript-eslint still load the TypeScript 5.x JavaScript API.
59
+ // Nx and typescript-eslint still load the TypeScript JS API (6.x).
48
60
  // Compilation is exclusively delegated to ttsc by the Nx plugin targets.
49
61
  { name: 'typescript', fallbackVersion: '^5.9.3', minimumVersion: '5.9.0', prefix: '^' },
50
62
  ];
@@ -52,7 +64,11 @@ const rootDevDependencies: RequiredDependency[] = [
52
64
  const cliPackageName = '@smoothbricks/cli';
53
65
 
54
66
  const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejandra', 'coreutils', 'gnutar'];
55
- const allowedNodePackages = ['nodejs_24', 'nodejs_latest'];
67
+ // Any explicit nodejs provider is fine — the pinned major is the repo's choice
68
+ // (Lambda parity, org convergence, …). Whether the pins in package.json agree
69
+ // with the runtime is validated against the live PATH (validateRootRuntimeVersions),
70
+ // never against a version template here.
71
+ const nodePackagePattern = /(^|\s)nodejs(_\d+|_latest)?(\s|#|$)/m;
56
72
 
57
73
  export async function applyToolConfigDefaults(root: string): Promise<void> {
58
74
  const context = readToolContext(root);
@@ -77,7 +93,7 @@ export async function applyRootDevDependencyDefaults(root: string, context: Tool
77
93
  return;
78
94
  }
79
95
  let changed = false;
80
- const devDependencies = getOrCreateRecord(pkg, 'devDependencies');
96
+ const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
81
97
  for (const dependency of rootDevDependencies) {
82
98
  const current = devDependencies[dependency.name];
83
99
  if (typeof current !== 'string' || !satisfiesDependencyPolicy(context.policy, current, dependency)) {
@@ -103,7 +119,7 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
103
119
  }
104
120
  let dependencyChanged = false;
105
121
  let workspaceChanged = false;
106
- const devDependencies = getOrCreateRecord(pkg, 'devDependencies');
122
+ const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
107
123
  for (const dependency of rootDevDependencies) {
108
124
  const current = devDependencies[dependency.name];
109
125
  if (typeof current !== 'string' || !satisfiesDependencyPolicy(context.policy, current, dependency)) {
@@ -134,12 +150,12 @@ export function applyToolingPackageDefaults(root: string, policy: ToolPolicy): v
134
150
  const path = join(root, 'tooling', 'package.json');
135
151
  const pkg = readJsonObject(path) ?? { name: policy.toolingPackageName, private: true, dependencies: {} };
136
152
  let changed = false;
137
- changed = setStringProperty(pkg, 'name', policy.toolingPackageName) || changed;
153
+ changed = setPackageStringField(pkg, 'name', policy.toolingPackageName) || changed;
138
154
  if (pkg.private !== true) {
139
155
  pkg.private = true;
140
156
  changed = true;
141
157
  }
142
- const dependencies = getOrCreateRecord(pkg, 'dependencies');
158
+ const dependencies = ensureDependencyMap(pkg, 'dependencies');
143
159
  changed = setStringProperty(dependencies, cliPackageName, policy.cliDependencyRange) || changed;
144
160
  if (changed || !existsSync(path)) {
145
161
  mkdirSync(dirname(path), { recursive: true });
@@ -171,7 +187,7 @@ export function applyDevenvPackageDefaults(root: string): void {
171
187
  }
172
188
  let content = readFileSync(path, 'utf8');
173
189
  let changed = false;
174
- if (!allowedNodePackages.some((name) => hasNixPackage(content, name))) {
190
+ if (!nodePackagePattern.test(content)) {
175
191
  const next = addNixPackage(content, 'nodejs_latest', '# Node.js for workspace tooling');
176
192
  changed = next !== content || changed;
177
193
  content = next;
@@ -198,7 +214,7 @@ export function validateRootDevDependencies(policy: ToolPolicy, rootPackage: Pac
198
214
  console.error('package.json not found or invalid');
199
215
  return 1;
200
216
  }
201
- const devDependencies = recordProperty(pkg, 'devDependencies');
217
+ const devDependencies = pkg.devDependencies;
202
218
  let failures = 0;
203
219
  for (const dependency of rootDevDependencies) {
204
220
  const version = devDependencies?.[dependency.name];
@@ -226,9 +242,9 @@ export function validateToolingPackage(root: string, policy: ToolPolicy): number
226
242
  console.error('tooling/package.json not found or invalid');
227
243
  return 1;
228
244
  }
229
- const dependencies = recordProperty(pkg, 'dependencies');
245
+ const dependencies = pkg.dependencies;
230
246
  let failures = 0;
231
- const actualName = typeof pkg.name === 'string' ? pkg.name : null;
247
+ const actualName = pkg.name ?? null;
232
248
  if (actualName !== policy.toolingPackageName) {
233
249
  console.error(`tooling/package.json name must be ${policy.toolingPackageName}`);
234
250
  failures++;
@@ -262,8 +278,10 @@ export function validateDevenvPackages(root: string): number {
262
278
  }
263
279
  const content = readFileSync(path, 'utf8');
264
280
  let failures = 0;
265
- if (!allowedNodePackages.some((name) => hasNixPackage(content, name))) {
266
- console.error(`tooling/direnv/devenv.nix packages must include ${allowedNodePackages.join(' or ')}`);
281
+ if (!nodePackagePattern.test(content)) {
282
+ console.error(
283
+ 'tooling/direnv/devenv.nix packages must include a nodejs provider (nodejs_<major> or nodejs_latest)',
284
+ );
267
285
  failures++;
268
286
  }
269
287
  for (const name of requiredDevenvPackages) {
@@ -334,7 +352,7 @@ function formatExpectedDependency(policy: ToolPolicy, dependency: RequiredDepend
334
352
  export function readToolContext(root: string): ToolContext {
335
353
  const rootPackage = readPackageJsonObject(join(root, 'package.json'));
336
354
  const toolingPackage = readJsonObject(join(root, 'tooling', 'package.json'));
337
- const configuredCliRange = recordProperty(toolingPackage ?? {}, 'dependencies')?.[cliPackageName];
355
+ const configuredCliRange = toolingPackage?.dependencies?.[cliPackageName];
338
356
  return {
339
357
  rootPackage,
340
358
  policy: toolPolicy(rootPackage, typeof configuredCliRange === 'string' ? configuredCliRange : null),
@@ -342,7 +360,7 @@ export function readToolContext(root: string): ToolContext {
342
360
  }
343
361
 
344
362
  function toolPolicy(rootPackage: PackageJson | null, configuredCliRange: string | null): ToolPolicy {
345
- const name = typeof rootPackage?.name === 'string' ? rootPackage.name : null;
363
+ const name = rootPackage?.name ?? null;
346
364
  const isCodebase = isSmoothBricksCodebasePackageName(name ?? undefined);
347
365
  const toolingName = toolingPackageName(name);
348
366
  // A locally linked prerelease can be newer than every published CLI. Keep an existing
@@ -435,14 +453,6 @@ function compareVersions(left: Version, right: Version): number {
435
453
  return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
436
454
  }
437
455
 
438
- function isRegistryPackument(value: unknown): value is { versions: Record<string, unknown> } {
439
- return typeof value === 'object' && value !== null && 'versions' in value && isObjectRecord(value.versions);
440
- }
441
-
442
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
443
- return typeof value === 'object' && value !== null && !Array.isArray(value);
444
- }
445
-
446
456
  function stripRangePrefix(version: string): string {
447
457
  return version.replace(/^[~^]/, '');
448
458
  }
@@ -456,7 +466,7 @@ function addWorkspacePattern(pkg: PackageJson, pattern: string): boolean {
456
466
  workspaces.push(pattern);
457
467
  return true;
458
468
  }
459
- if (isObjectRecord(workspaces) && Array.isArray(workspaces.packages)) {
469
+ if (workspaces && Array.isArray(workspaces.packages)) {
460
470
  if (workspaces.packages.includes(pattern)) {
461
471
  return false;
462
472
  }
@@ -472,5 +482,5 @@ function hasWorkspacePattern(pkg: PackageJson, pattern: string): boolean {
472
482
  if (Array.isArray(workspaces)) {
473
483
  return workspaces.includes(pattern);
474
484
  }
475
- return isObjectRecord(workspaces) && Array.isArray(workspaces.packages) && workspaces.packages.includes(pattern);
485
+ return Boolean(workspaces && Array.isArray(workspaces.packages) && workspaces.packages.includes(pattern));
476
486
  }
@@ -2,6 +2,7 @@ import { describe, expect, it, spyOn } from 'bun:test';
2
2
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { type PackageJson, parsePackageJsonText } from '../lib/json.js';
5
6
  import { applyWranglerDefaults, firstWranglerEnv, validateWrangler } from './wrangler.js';
6
7
 
7
8
  describe('firstWranglerEnv', () => {
@@ -42,7 +43,7 @@ describe('applyWranglerDefaults', () => {
42
43
  },
43
44
  });
44
45
  const typecheck = nxTargets(pkg).typecheck;
45
- expect(isRecord(typecheck) ? typecheck.dependsOn : null).toEqual(['^build', 'wrangler-types']);
46
+ expect(typecheck?.dependsOn ?? null).toEqual(['^build', 'wrangler-types']);
46
47
 
47
48
  const logs = captureConsoleLogs();
48
49
  applyWranglerDefaults(root);
@@ -63,8 +64,7 @@ describe('applyWranglerDefaults', () => {
63
64
  try {
64
65
  applyWranglerDefaults(root);
65
66
  const target = nxTargets(await readJson(join(root, 'packages/api/package.json')))['wrangler-types'];
66
- const options = isRecord(target) ? target.options : null;
67
- expect(isRecord(options) ? options.command : null).toBe(
67
+ expect(target?.options?.command ?? null).toBe(
68
68
  'wrangler types --env-file .dev.vars.example --include-runtime false',
69
69
  );
70
70
  } finally {
@@ -187,27 +187,23 @@ async function createWorkspace(
187
187
  return root;
188
188
  }
189
189
 
190
- function nxTargets(pkg: Record<string, unknown>): Record<string, unknown> {
191
- const nx = pkg.nx;
192
- if (!isRecord(nx) || !isRecord(nx.targets)) {
190
+ function nxTargets(pkg: PackageJson): NonNullable<NonNullable<PackageJson['nx']>['targets']> {
191
+ const targets = pkg.nx?.targets;
192
+ if (!targets) {
193
193
  throw new Error('nx.targets not found');
194
194
  }
195
- return nx.targets;
195
+ return targets;
196
196
  }
197
197
 
198
- function isRecord(value: unknown): value is Record<string, unknown> {
199
- return typeof value === 'object' && value !== null && !Array.isArray(value);
200
- }
201
-
202
- async function readJson(path: string): Promise<Record<string, unknown>> {
203
- const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
204
- if (!isRecord(parsed)) {
198
+ async function readJson(path: string): Promise<PackageJson> {
199
+ const parsed = parsePackageJsonText(await readFile(path, 'utf8'));
200
+ if (!parsed) {
205
201
  throw new Error('expected JSON object');
206
202
  }
207
203
  return parsed;
208
204
  }
209
205
 
210
- async function writeJson(path: string, value: Record<string, unknown>): Promise<void> {
206
+ async function writeJson(path: string, value: PackageJson): Promise<void> {
211
207
  await mkdir(join(path, '..'), { recursive: true });
212
208
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
213
209
  }
@@ -1,13 +1,13 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join, relative } from 'node:path';
3
- import { getOrCreateRecord, recordProperty, writeJsonObject } from '../lib/json.js';
2
+ import { join } from 'node:path';
3
+ import { ensureNx, ensureNxTargets, type NxTargetConfig, type PackageJson, writeJsonObject } from '../lib/json.js';
4
4
  import { getWorkspacePackageManifests } from '../lib/workspace.js';
5
5
 
6
6
  interface WranglerProject {
7
7
  label: string;
8
8
  dir: string;
9
9
  packageJsonPath: string;
10
- json: Record<string, unknown>;
10
+ json: PackageJson;
11
11
  tomlPath: string;
12
12
  }
13
13
 
@@ -20,13 +20,14 @@ export function firstWranglerEnv(tomlText: string): string | null {
20
20
  function wranglerProjects(root: string): WranglerProject[] {
21
21
  const projects: WranglerProject[] = [];
22
22
  for (const pkg of getWorkspacePackageManifests(root)) {
23
- const tomlPath = join(pkg.path, 'wrangler.toml');
23
+ const dir = join(root, pkg.path);
24
+ const tomlPath = join(dir, 'wrangler.toml');
24
25
  if (!existsSync(tomlPath)) {
25
26
  continue;
26
27
  }
27
28
  projects.push({
28
- label: relative(root, pkg.path) || '.',
29
- dir: pkg.path,
29
+ label: pkg.path || '.',
30
+ dir,
30
31
  packageJsonPath: pkg.packageJsonPath,
31
32
  json: pkg.json,
32
33
  tomlPath,
@@ -56,9 +57,9 @@ export function applyWranglerDefaults(root: string): void {
56
57
  }
57
58
  for (const project of projects) {
58
59
  const env = firstWranglerEnv(readFileSync(project.tomlPath, 'utf8'));
59
- const nx = getOrCreateRecord(project.json, 'nx');
60
- const targets = getOrCreateRecord(nx, 'targets');
61
- const desired: Record<string, unknown> = {
60
+ const nx = ensureNx(project.json);
61
+ const targets = ensureNxTargets(nx);
62
+ const desired: NxTargetConfig = {
62
63
  executor: 'nx:run-commands',
63
64
  cache: true,
64
65
  inputs: ['{projectRoot}/wrangler.toml', '{projectRoot}/.dev.vars.example'],
@@ -69,14 +70,14 @@ export function applyWranglerDefaults(root: string): void {
69
70
  },
70
71
  };
71
72
  let changed = false;
72
- const existing = recordProperty(targets, 'wrangler-types');
73
+ const existing = targets['wrangler-types'];
73
74
  if (!existing || JSON.stringify(existing) !== JSON.stringify(desired)) {
74
75
  targets['wrangler-types'] = desired;
75
76
  changed = true;
76
77
  }
77
- const typecheck = recordProperty(targets, 'typecheck');
78
+ const typecheck = targets.typecheck;
78
79
  if (typecheck) {
79
- const dependsOn: unknown[] = Array.isArray(typecheck.dependsOn) ? typecheck.dependsOn : [];
80
+ const dependsOn = Array.isArray(typecheck.dependsOn) ? [...typecheck.dependsOn] : [];
80
81
  if (!dependsOn.includes('wrangler-types')) {
81
82
  dependsOn.push('wrangler-types');
82
83
  typecheck.dependsOn = dependsOn;
@@ -116,9 +117,8 @@ export function validateWrangler(root: string): number {
116
117
  console.log(`⨯ ${project.label}: missing .dev.vars.example`);
117
118
  projectProblems++;
118
119
  }
119
- const nx = recordProperty(project.json, 'nx');
120
- const targets = nx ? recordProperty(nx, 'targets') : null;
121
- if (!targets || !recordProperty(targets, 'wrangler-types')) {
120
+ const targets = project.json.nx?.targets;
121
+ if (!targets?.['wrangler-types']) {
122
122
  console.log(`⨯ ${project.label}: missing wrangler-types nx target`);
123
123
  projectProblems++;
124
124
  }
@@ -7,7 +7,10 @@ import {
7
7
  nxShowProjectCommand,
8
8
  projectNamesFromNxShowProjectsOutput,
9
9
  projectNamesWithTarget,
10
+ projectRootFromNxProjectJson,
11
+ targetDependenciesFromNxProjectJson,
10
12
  targetNamesFromNxProjectJson,
13
+ targetOutputsFromNxProjectJson,
11
14
  } from './index.js';
12
15
 
13
16
  describe('Nx helper command construction', () => {
@@ -67,6 +70,46 @@ describe('Nx helper output formatting', () => {
67
70
  ]);
68
71
  });
69
72
 
73
+ it('extracts resolved roots, output declarations, and same-project target dependencies', () => {
74
+ const metadata = {
75
+ root: 'packages/native',
76
+ targets: {
77
+ 'build-macos': {
78
+ outputs: ['{projectRoot}/dist/*.dmg'],
79
+ dependsOn: ['^build', 'compile-macos', { target: 'package-macos', projects: 'self' }],
80
+ },
81
+ test: { outputs: [] },
82
+ },
83
+ };
84
+
85
+ expect(projectRootFromNxProjectJson(metadata)).toBe('packages/native');
86
+ expect(targetOutputsFromNxProjectJson(metadata)).toEqual(
87
+ new Map([
88
+ ['build-macos', ['{projectRoot}/dist/*.dmg']],
89
+ ['test', []],
90
+ ]),
91
+ );
92
+ expect(targetDependenciesFromNxProjectJson(metadata)).toEqual(
93
+ new Map([['build-macos', ['^build', 'compile-macos', 'package-macos']]]),
94
+ );
95
+ });
96
+
97
+ it('omits cross-project dependencies from same-project target closures', () => {
98
+ expect(
99
+ targetDependenciesFromNxProjectJson({
100
+ targets: {
101
+ 'rust-wasm': {
102
+ dependsOn: [
103
+ 'prepare',
104
+ { target: 'cargo-wasm', projects: ['lmao-rs'] },
105
+ { target: 'build', projects: 'dependencies' },
106
+ ],
107
+ },
108
+ },
109
+ }),
110
+ ).toEqual(new Map([['rust-wasm', ['prepare']]]));
111
+ });
112
+
70
113
  it('treats missing target metadata as an empty project', () => {
71
114
  expect(targetNamesFromNxProjectJson({ name: 'cli' })).toEqual([]);
72
115
  });