@smoothbricks/cli 0.10.3 → 0.10.5

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 (115) hide show
  1. package/README.md +9 -13
  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 +13 -5
  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.map +1 -1
  9. package/dist/github-ci/index.js +52 -16
  10. package/dist/github-ci/outputs.d.ts.map +1 -1
  11. package/dist/github-ci/outputs.js +1 -2
  12. package/dist/lib/cli-package.js +2 -2
  13. package/dist/lib/json.d.ts +120 -11
  14. package/dist/lib/json.d.ts.map +1 -1
  15. package/dist/lib/json.js +319 -29
  16. package/dist/lib/workspace.d.ts +7 -14
  17. package/dist/lib/workspace.d.ts.map +1 -1
  18. package/dist/lib/workspace.js +76 -82
  19. package/dist/monorepo/ci-workflow.d.ts +2 -0
  20. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  21. package/dist/monorepo/ci-workflow.js +27 -1
  22. package/dist/monorepo/git-config.js +2 -2
  23. package/dist/monorepo/index.d.ts +4 -2
  24. package/dist/monorepo/index.d.ts.map +1 -1
  25. package/dist/monorepo/index.js +19 -3
  26. package/dist/monorepo/lockfile.d.ts +14 -1
  27. package/dist/monorepo/lockfile.d.ts.map +1 -1
  28. package/dist/monorepo/lockfile.js +33 -21
  29. package/dist/monorepo/managed-files.d.ts +11 -0
  30. package/dist/monorepo/managed-files.d.ts.map +1 -1
  31. package/dist/monorepo/managed-files.js +57 -32
  32. package/dist/monorepo/package-policy.d.ts.map +1 -1
  33. package/dist/monorepo/package-policy.js +52 -45
  34. package/dist/monorepo/packed-manifest.d.ts +5 -2
  35. package/dist/monorepo/packed-manifest.d.ts.map +1 -1
  36. package/dist/monorepo/packed-manifest.js +16 -47
  37. package/dist/monorepo/packed-package.d.ts.map +1 -1
  38. package/dist/monorepo/packed-package.js +28 -16
  39. package/dist/monorepo/packs/index.d.ts.map +1 -1
  40. package/dist/monorepo/packs/index.js +14 -5
  41. package/dist/monorepo/publish-workflow.js +3 -1
  42. package/dist/monorepo/runtime.d.ts +15 -0
  43. package/dist/monorepo/runtime.d.ts.map +1 -1
  44. package/dist/monorepo/runtime.js +66 -12
  45. package/dist/monorepo/tool-validation.d.ts +2 -2
  46. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  47. package/dist/monorepo/tool-validation.js +141 -38
  48. package/dist/monorepo/wrangler.d.ts.map +1 -1
  49. package/dist/monorepo/wrangler.js +13 -13
  50. package/dist/nx/index.d.ts +9 -7
  51. package/dist/nx/index.d.ts.map +1 -1
  52. package/dist/nx/index.js +48 -31
  53. package/dist/pr/index.d.ts.map +1 -1
  54. package/dist/pr/index.js +28 -32
  55. package/dist/release/candidates.d.ts +3 -2
  56. package/dist/release/candidates.d.ts.map +1 -1
  57. package/dist/release/candidates.js +4 -5
  58. package/dist/release/index.d.ts +1 -0
  59. package/dist/release/index.d.ts.map +1 -1
  60. package/dist/release/index.js +88 -59
  61. package/dist/wrangler/prepare-env.d.ts +0 -1
  62. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  63. package/dist/wrangler/prepare-env.js +100 -51
  64. package/dist/wrangler/scaffold.d.ts.map +1 -1
  65. package/dist/wrangler/scaffold.js +12 -12
  66. package/managed/raw/envrc +5 -3
  67. package/managed/raw/git-format-staged.yml +19 -1
  68. package/managed/raw/patches/ttsc@0.19.3.patch +67 -0
  69. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +2 -1
  70. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  71. package/managed/raw/tooling/direnv/setup-environment.ts +170 -19
  72. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  73. package/managed/templates/github/actions/cache-node-modules/action.yml +5 -2
  74. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  75. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  76. package/managed/templates/github/workflows/managed-files.yml +80 -0
  77. package/package.json +15 -4
  78. package/src/bun/preload.ts +12 -0
  79. package/src/cli.ts +20 -8
  80. package/src/generate/index.ts +0 -6
  81. package/src/github-ci/index.ts +49 -18
  82. package/src/github-ci/outputs.ts +1 -2
  83. package/src/lib/cli-package.ts +2 -2
  84. package/src/lib/json.ts +293 -32
  85. package/src/lib/workspace.ts +88 -100
  86. package/src/monorepo/__tests__/ci-workflow.test.ts +3 -3
  87. package/src/monorepo/ci-workflow.ts +27 -1
  88. package/src/monorepo/git-config.ts +2 -2
  89. package/src/monorepo/index.ts +19 -3
  90. package/src/monorepo/lockfile.test.ts +35 -36
  91. package/src/monorepo/lockfile.ts +49 -24
  92. package/src/monorepo/managed-files.test.ts +1 -0
  93. package/src/monorepo/managed-files.ts +62 -38
  94. package/src/monorepo/package-policy.test.ts +20 -50
  95. package/src/monorepo/package-policy.ts +76 -59
  96. package/src/monorepo/packed-manifest.ts +18 -52
  97. package/src/monorepo/packed-package.ts +43 -22
  98. package/src/monorepo/packs/index.ts +14 -5
  99. package/src/monorepo/publish-workflow.ts +3 -1
  100. package/src/monorepo/runtime.test.ts +42 -3
  101. package/src/monorepo/runtime.ts +90 -12
  102. package/src/monorepo/tool-validation.test.ts +38 -16
  103. package/src/monorepo/tool-validation.ts +149 -41
  104. package/src/monorepo/wrangler.test.ts +11 -15
  105. package/src/monorepo/wrangler.ts +15 -15
  106. package/src/nx/index.test.ts +9 -5
  107. package/src/nx/index.ts +69 -42
  108. package/src/pr/index.ts +42 -36
  109. package/src/release/__tests__/candidates.test.ts +5 -4
  110. package/src/release/__tests__/trust-publisher.test.ts +7 -0
  111. package/src/release/candidates.ts +10 -17
  112. package/src/release/index.ts +110 -73
  113. package/src/wrangler/prepare-env.ts +75 -54
  114. package/src/wrangler/scaffold.test.ts +9 -12
  115. package/src/wrangler/scaffold.ts +13 -13
@@ -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,12 @@ export interface ToolContext {
23
30
  policy: ToolPolicy;
24
31
  }
25
32
 
33
+ interface RegistryPackument {
34
+ versions: Record<string, unknown>;
35
+ 'dist-tags'?: Record<string, string>;
36
+ }
37
+
38
+ const isRegistryPackument = typia.createIs<RegistryPackument>();
26
39
  const rootDevDependencies: RequiredDependency[] = [
27
40
  { name: '@biomejs/biome', fallbackVersion: '^2.3.5', minimumVersion: '2.3.0', prefix: '^' },
28
41
  { name: '@nx/js', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
@@ -43,28 +56,40 @@ const rootDevDependencies: RequiredDependency[] = [
43
56
  },
44
57
  { name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
45
58
  { name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', prefix: '^' },
46
- { 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
+ // Exact pin: Bun patchedDependencies keys are version-locked (see patches/ttsc@0.19.3.patch).
60
+ { name: 'ttsc', fallbackVersion: '0.19.3' },
61
+ // Nx and typescript-eslint still load the TypeScript JS API (6.x).
48
62
  // Compilation is exclusively delegated to ttsc by the Nx plugin targets.
49
- { name: 'typescript', fallbackVersion: '^5.9.3', minimumVersion: '5.9.0', prefix: '^' },
63
+ { name: 'typescript', fallbackVersion: '^6.0.3', minimumVersion: '6.0.0', prefix: '^' },
64
+ // TS7 native compiler for ttsc (TTSC_TSGO_BINARY). Not the unscoped API package.
65
+ // npm alias form required — Bun cannot install @typescript/typescript6 cleanly (bun#33834).
66
+ { name: '@typescript/native', fallbackVersion: 'npm:typescript@^7.0.2' },
50
67
  ];
51
68
 
52
69
  const cliPackageName = '@smoothbricks/cli';
53
70
 
54
71
  const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejandra', 'coreutils', 'gnutar'];
55
- const allowedNodePackages = ['nodejs_24', 'nodejs_latest'];
72
+ // Any explicit nodejs provider is fine — the pinned major is the repo's choice
73
+ // (Lambda parity, org convergence, …). Whether the pins in package.json agree
74
+ // with the runtime is validated against the live PATH (validateRootRuntimeVersions),
75
+ // never against a version template here.
76
+ const nodePackagePattern = /(^|\s)nodejs(_\d+|_latest)?(\s|#|$)/m;
77
+
78
+ const TTSC_PATCHED_DEPENDENCY_KEY = 'ttsc@0.19.3';
79
+ const TTSC_PATCH_PATH = 'patches/ttsc@0.19.3.patch';
56
80
 
57
81
  export async function applyToolConfigDefaults(root: string): Promise<void> {
58
- const context = readToolContext(root);
82
+ const context = await readToolContext(root);
59
83
  await applyRootPackageToolDefaults(root, context);
60
84
  applyToolingPackageDefaults(root, context.policy);
61
85
  applyDevenvPackageDefaults(root);
62
86
  }
63
87
 
64
- export function validateToolConfig(root: string): number {
65
- const context = readToolContext(root);
88
+ export async function validateToolConfig(root: string): Promise<number> {
89
+ const context = await readToolContext(root);
66
90
  return (
67
91
  validateRootDevDependencies(context.policy, context.rootPackage) +
92
+ validateTtscPatchedDependency(context.rootPackage) +
68
93
  validateToolingPackage(root, context.policy) +
69
94
  validateToolingWorkspace(context.rootPackage) +
70
95
  validateDevenvPackages(root)
@@ -77,7 +102,7 @@ export async function applyRootDevDependencyDefaults(root: string, context: Tool
77
102
  return;
78
103
  }
79
104
  let changed = false;
80
- const devDependencies = getOrCreateRecord(pkg, 'devDependencies');
105
+ const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
81
106
  for (const dependency of rootDevDependencies) {
82
107
  const current = devDependencies[dependency.name];
83
108
  if (typeof current !== 'string' || !satisfiesDependencyPolicy(context.policy, current, dependency)) {
@@ -88,6 +113,7 @@ export async function applyRootDevDependencyDefaults(root: string, context: Tool
88
113
  if (delete devDependencies['@smoothbricks/cli']) {
89
114
  changed = true;
90
115
  }
116
+ changed = ensureTtscPatchedDependency(pkg) || changed;
91
117
  if (changed) {
92
118
  writeJsonObject(join(root, 'package.json'), pkg);
93
119
  console.log('updated package.json workspace tool dependencies');
@@ -103,7 +129,8 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
103
129
  }
104
130
  let dependencyChanged = false;
105
131
  let workspaceChanged = false;
106
- const devDependencies = getOrCreateRecord(pkg, 'devDependencies');
132
+ let patchChanged = false;
133
+ const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
107
134
  for (const dependency of rootDevDependencies) {
108
135
  const current = devDependencies[dependency.name];
109
136
  if (typeof current !== 'string' || !satisfiesDependencyPolicy(context.policy, current, dependency)) {
@@ -115,7 +142,8 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
115
142
  dependencyChanged = true;
116
143
  }
117
144
  workspaceChanged = addWorkspacePattern(pkg, 'tooling');
118
- if (dependencyChanged || workspaceChanged) {
145
+ patchChanged = ensureTtscPatchedDependency(pkg);
146
+ if (dependencyChanged || workspaceChanged || patchChanged) {
119
147
  writeJsonObject(join(root, 'package.json'), pkg);
120
148
  }
121
149
  console.log(
@@ -128,18 +156,23 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
128
156
  ? 'updated package.json tooling workspace'
129
157
  : 'unchanged package.json tooling workspace',
130
158
  );
159
+ console.log(
160
+ patchChanged
161
+ ? 'updated package.json patchedDependencies for ttsc'
162
+ : 'unchanged package.json patchedDependencies for ttsc',
163
+ );
131
164
  }
132
165
 
133
166
  export function applyToolingPackageDefaults(root: string, policy: ToolPolicy): void {
134
167
  const path = join(root, 'tooling', 'package.json');
135
168
  const pkg = readJsonObject(path) ?? { name: policy.toolingPackageName, private: true, dependencies: {} };
136
169
  let changed = false;
137
- changed = setStringProperty(pkg, 'name', policy.toolingPackageName) || changed;
170
+ changed = setPackageStringField(pkg, 'name', policy.toolingPackageName) || changed;
138
171
  if (pkg.private !== true) {
139
172
  pkg.private = true;
140
173
  changed = true;
141
174
  }
142
- const dependencies = getOrCreateRecord(pkg, 'dependencies');
175
+ const dependencies = ensureDependencyMap(pkg, 'dependencies');
143
176
  changed = setStringProperty(dependencies, cliPackageName, policy.cliDependencyRange) || changed;
144
177
  if (changed || !existsSync(path)) {
145
178
  mkdirSync(dirname(path), { recursive: true });
@@ -171,7 +204,7 @@ export function applyDevenvPackageDefaults(root: string): void {
171
204
  }
172
205
  let content = readFileSync(path, 'utf8');
173
206
  let changed = false;
174
- if (!allowedNodePackages.some((name) => hasNixPackage(content, name))) {
207
+ if (!nodePackagePattern.test(content)) {
175
208
  const next = addNixPackage(content, 'nodejs_latest', '# Node.js for workspace tooling');
176
209
  changed = next !== content || changed;
177
210
  content = next;
@@ -198,7 +231,7 @@ export function validateRootDevDependencies(policy: ToolPolicy, rootPackage: Pac
198
231
  console.error('package.json not found or invalid');
199
232
  return 1;
200
233
  }
201
- const devDependencies = recordProperty(pkg, 'devDependencies');
234
+ const devDependencies = pkg.devDependencies;
202
235
  let failures = 0;
203
236
  for (const dependency of rootDevDependencies) {
204
237
  const version = devDependencies?.[dependency.name];
@@ -219,6 +252,26 @@ export function validateRootDevDependencies(policy: ToolPolicy, rootPackage: Pac
219
252
  return failures;
220
253
  }
221
254
 
255
+ function ensureTtscPatchedDependency(pkg: PackageJson): boolean {
256
+ const patched = ensureDependencyMap(pkg, 'patchedDependencies');
257
+ return setStringProperty(patched, TTSC_PATCHED_DEPENDENCY_KEY, TTSC_PATCH_PATH);
258
+ }
259
+
260
+ function validateTtscPatchedDependency(rootPackage: PackageJson | null): number {
261
+ if (!rootPackage) {
262
+ return 0;
263
+ }
264
+ const actual = rootPackage.patchedDependencies?.[TTSC_PATCHED_DEPENDENCY_KEY];
265
+ if (actual === TTSC_PATCH_PATH) {
266
+ return 0;
267
+ }
268
+ console.error(
269
+ `package.json patchedDependencies.${TTSC_PATCHED_DEPENDENCY_KEY} must be ${TTSC_PATCH_PATH}` +
270
+ (typeof actual === 'string' ? `; found ${actual}` : ' (required for declaration-emit fix)'),
271
+ );
272
+ return 1;
273
+ }
274
+
222
275
  export function validateToolingPackage(root: string, policy: ToolPolicy): number {
223
276
  const path = join(root, 'tooling', 'package.json');
224
277
  const pkg = readJsonObject(path);
@@ -226,9 +279,9 @@ export function validateToolingPackage(root: string, policy: ToolPolicy): number
226
279
  console.error('tooling/package.json not found or invalid');
227
280
  return 1;
228
281
  }
229
- const dependencies = recordProperty(pkg, 'dependencies');
282
+ const dependencies = pkg.dependencies;
230
283
  let failures = 0;
231
- const actualName = typeof pkg.name === 'string' ? pkg.name : null;
284
+ const actualName = pkg.name ?? null;
232
285
  if (actualName !== policy.toolingPackageName) {
233
286
  console.error(`tooling/package.json name must be ${policy.toolingPackageName}`);
234
287
  failures++;
@@ -262,8 +315,10 @@ export function validateDevenvPackages(root: string): number {
262
315
  }
263
316
  const content = readFileSync(path, 'utf8');
264
317
  let failures = 0;
265
- if (!allowedNodePackages.some((name) => hasNixPackage(content, name))) {
266
- console.error(`tooling/direnv/devenv.nix packages must include ${allowedNodePackages.join(' or ')}`);
318
+ if (!nodePackagePattern.test(content)) {
319
+ console.error(
320
+ 'tooling/direnv/devenv.nix packages must include a nodejs provider (nodejs_<major> or nodejs_latest)',
321
+ );
267
322
  failures++;
268
323
  }
269
324
  for (const name of requiredDevenvPackages) {
@@ -313,6 +368,10 @@ function satisfiesDependencyPolicy(policy: ToolPolicy, version: string, dependen
313
368
  if (workspaceDependencyExpected(policy, dependency)) {
314
369
  return version === 'workspace:*';
315
370
  }
371
+ // Dual-package native compiler: must stay an npm:typescript@7 alias, never unscoped "typescript".
372
+ if (dependency.name === '@typescript/native') {
373
+ return isTypeScriptNativeAlias(version);
374
+ }
316
375
  if (dependency.minimumVersion === undefined) {
317
376
  return version === dependency.fallbackVersion;
318
377
  }
@@ -324,37 +383,41 @@ function satisfiesDependencyPolicy(policy: ToolPolicy, version: string, dependen
324
383
  return compareVersions(parsed, minimum) >= 0;
325
384
  }
326
385
 
386
+ function isTypeScriptNativeAlias(version: string): boolean {
387
+ // Accept npm:typescript@7, npm:typescript@^7.0.2, npm:typescript@~7.0.2, etc.
388
+ return /^npm:typescript@(?:\^|~)?7(?:\.|$)/.test(version.trim());
389
+ }
390
+
327
391
  function formatExpectedDependency(policy: ToolPolicy, dependency: RequiredDependency): string {
328
392
  if (workspaceDependencyExpected(policy, dependency)) {
329
393
  return 'workspace:*';
330
394
  }
395
+ if (dependency.name === '@typescript/native') {
396
+ return 'npm:typescript@^7 (TypeScript 7 native compiler alias for ttsc)';
397
+ }
331
398
  return dependency.minimumVersion ? `>= ${dependency.minimumVersion}` : dependency.fallbackVersion;
332
399
  }
333
400
 
334
- export function readToolContext(root: string): ToolContext {
401
+ export async function readToolContext(root: string): Promise<ToolContext> {
335
402
  const rootPackage = readPackageJsonObject(join(root, 'package.json'));
336
403
  const toolingPackage = readJsonObject(join(root, 'tooling', 'package.json'));
337
- const configuredCliRange = recordProperty(toolingPackage ?? {}, 'dependencies')?.[cliPackageName];
404
+ const configuredCliRange = toolingPackage?.dependencies?.[cliPackageName];
338
405
  return {
339
406
  rootPackage,
340
- policy: toolPolicy(rootPackage, typeof configuredCliRange === 'string' ? configuredCliRange : null),
407
+ policy: await toolPolicy(rootPackage, typeof configuredCliRange === 'string' ? configuredCliRange : null),
341
408
  };
342
409
  }
343
410
 
344
- function toolPolicy(rootPackage: PackageJson | null, configuredCliRange: string | null): ToolPolicy {
345
- const name = typeof rootPackage?.name === 'string' ? rootPackage.name : null;
411
+ async function toolPolicy(rootPackage: PackageJson | null, configuredCliRange: string | null): Promise<ToolPolicy> {
412
+ const name = rootPackage?.name ?? null;
346
413
  const isCodebase = isSmoothBricksCodebasePackageName(name ?? undefined);
347
414
  const toolingName = toolingPackageName(name);
348
- // A locally linked prerelease can be newer than every published CLI. Keep an existing
349
- // installable range instead of rewriting the consumer manifest to an unpublished version.
350
- const publishedCliRange =
351
- cliPackageVersion.includes('-') && configuredCliRange && parseVersion(configuredCliRange)
352
- ? configuredCliRange
353
- : `^${cliPackageVersion}`;
354
415
  return {
355
416
  isSmoothBricksCodebase: isCodebase,
356
417
  toolingPackageName: toolingName,
357
- cliDependencyRange: isCodebase ? 'workspace:*' : publishedCliRange,
418
+ // Consumers pin the latest *published* CLI. Running a linked prerelease must not
419
+ // freeze tooling/package.json on an older range or rewrite it to an unpublished -next.
420
+ cliDependencyRange: isCodebase ? 'workspace:*' : await resolvePublishedCliDependencyRange(configuredCliRange),
358
421
  };
359
422
  }
360
423
 
@@ -379,6 +442,59 @@ async function resolveDependencyVersion(policy: ToolPolicy, dependency: Required
379
442
  return `${dependency.prefix ?? ''}${latest ?? stripRangePrefix(dependency.fallbackVersion)}`;
380
443
  }
381
444
 
445
+ /**
446
+ * Consumer monorepos pin `@smoothbricks/cli` to the latest stable release.
447
+ * Prefer npm `dist-tags.latest`. Never write a running `-next` package version into
448
+ * consumer manifests; never leave an older pin just because the running CLI is prerelease.
449
+ */
450
+ async function resolvePublishedCliDependencyRange(configuredCliRange: string | null): Promise<string> {
451
+ const latest = await fetchLatestStableVersion(cliPackageName);
452
+ if (latest) {
453
+ return `^${latest}`;
454
+ }
455
+ // Registry unavailable: if the running CLI is a stable publish, use it.
456
+ if (!cliPackageVersion.includes('-')) {
457
+ return `^${cliPackageVersion}`;
458
+ }
459
+ // Linked prerelease + offline down: keep an existing installable stable pin.
460
+ if (configuredCliRange && parseVersion(configuredCliRange)) {
461
+ return configuredCliRange;
462
+ }
463
+ // Last resort: caret of the prerelease base (0.10.5-next.0 → ^0.10.5) so manifests stay valid.
464
+ const base = parseVersion(cliPackageVersion);
465
+ return base ? `^${base.major}.${base.minor}.${base.patch}` : `^${cliPackageVersion}`;
466
+ }
467
+
468
+ async function fetchLatestStableVersion(packageName: string): Promise<string | null> {
469
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace('%40', '@')}`;
470
+ const response = await fetch(url, { headers: { accept: 'application/vnd.npm.install-v1+json' } });
471
+ if (!response.ok) {
472
+ return null;
473
+ }
474
+ const body: unknown = await response.json();
475
+ if (!isRegistryPackument(body)) {
476
+ return null;
477
+ }
478
+ const tagged = body['dist-tags']?.latest;
479
+ if (typeof tagged === 'string' && parseVersion(tagged) && !tagged.includes('-')) {
480
+ return tagged;
481
+ }
482
+ let latest: Version | null = null;
483
+ for (const raw of Object.keys(body.versions)) {
484
+ if (raw.includes('-')) {
485
+ continue;
486
+ }
487
+ const version = parseVersion(raw);
488
+ if (!version) {
489
+ continue;
490
+ }
491
+ if (!latest || compareVersions(version, latest) > 0) {
492
+ latest = version;
493
+ }
494
+ }
495
+ return latest ? `${latest.major}.${latest.minor}.${latest.patch}` : null;
496
+ }
497
+
382
498
  async function fetchLatestPatchVersion(packageName: string, minimumVersion: string): Promise<string | null> {
383
499
  const minorRange = sameMajorMinorRange(minimumVersion);
384
500
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace('%40', '@')}`;
@@ -435,14 +551,6 @@ function compareVersions(left: Version, right: Version): number {
435
551
  return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
436
552
  }
437
553
 
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
554
  function stripRangePrefix(version: string): string {
447
555
  return version.replace(/^[~^]/, '');
448
556
  }
@@ -456,7 +564,7 @@ function addWorkspacePattern(pkg: PackageJson, pattern: string): boolean {
456
564
  workspaces.push(pattern);
457
565
  return true;
458
566
  }
459
- if (isObjectRecord(workspaces) && Array.isArray(workspaces.packages)) {
567
+ if (workspaces && Array.isArray(workspaces.packages)) {
460
568
  if (workspaces.packages.includes(pattern)) {
461
569
  return false;
462
570
  }
@@ -472,5 +580,5 @@ function hasWorkspacePattern(pkg: PackageJson, pattern: string): boolean {
472
580
  if (Array.isArray(workspaces)) {
473
581
  return workspaces.includes(pattern);
474
582
  }
475
- return isObjectRecord(workspaces) && Array.isArray(workspaces.packages) && workspaces.packages.includes(pattern);
583
+ return Boolean(workspaces && Array.isArray(workspaces.packages) && workspaces.packages.includes(pattern));
476
584
  }
@@ -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
  }
@@ -94,16 +94,20 @@ describe('Nx helper output formatting', () => {
94
94
  );
95
95
  });
96
96
 
97
- it('rejects unsupported cross-project object target dependencies', () => {
98
- expect(() =>
97
+ it('omits cross-project dependencies from same-project target closures', () => {
98
+ expect(
99
99
  targetDependenciesFromNxProjectJson({
100
100
  targets: {
101
- build: {
102
- dependsOn: [{ target: 'build', projects: 'dependencies' }],
101
+ 'rust-wasm': {
102
+ dependsOn: [
103
+ 'prepare',
104
+ { target: 'cargo-wasm', projects: ['lmao-rs'] },
105
+ { target: 'build', projects: 'dependencies' },
106
+ ],
103
107
  },
104
108
  },
105
109
  }),
106
- ).toThrow('unsupported cross-project dependsOn');
110
+ ).toEqual(new Map([['rust-wasm', ['prepare']]]));
107
111
  });
108
112
 
109
113
  it('treats missing target metadata as an empty project', () => {