@smoothbricks/cli 0.10.3 → 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 (111) 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 +12 -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.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 +118 -11
  14. package/dist/lib/json.d.ts.map +1 -1
  15. package/dist/lib/json.js +317 -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 +3 -1
  24. package/dist/monorepo/index.d.ts.map +1 -1
  25. package/dist/monorepo/index.js +6 -2
  26. package/dist/monorepo/lockfile.d.ts +13 -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 +52 -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 +13 -4
  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.map +1 -1
  46. package/dist/monorepo/tool-validation.js +32 -23
  47. package/dist/monorepo/wrangler.d.ts.map +1 -1
  48. package/dist/monorepo/wrangler.js +13 -13
  49. package/dist/nx/index.d.ts +9 -7
  50. package/dist/nx/index.d.ts.map +1 -1
  51. package/dist/nx/index.js +48 -31
  52. package/dist/pr/index.d.ts.map +1 -1
  53. package/dist/pr/index.js +28 -32
  54. package/dist/release/candidates.d.ts +3 -2
  55. package/dist/release/candidates.d.ts.map +1 -1
  56. package/dist/release/candidates.js +4 -5
  57. package/dist/release/index.d.ts +1 -0
  58. package/dist/release/index.d.ts.map +1 -1
  59. package/dist/release/index.js +82 -59
  60. package/dist/wrangler/prepare-env.d.ts +0 -1
  61. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  62. package/dist/wrangler/prepare-env.js +100 -51
  63. package/dist/wrangler/scaffold.d.ts.map +1 -1
  64. package/dist/wrangler/scaffold.js +12 -12
  65. package/managed/raw/git-format-staged.yml +19 -1
  66. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +2 -1
  67. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  68. package/managed/raw/tooling/direnv/setup-environment.ts +150 -19
  69. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  70. package/managed/templates/github/actions/cache-node-modules/action.yml +5 -2
  71. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  72. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  73. package/managed/templates/github/workflows/managed-files.yml +80 -0
  74. package/package.json +15 -4
  75. package/src/bun/preload.ts +12 -0
  76. package/src/cli.ts +19 -7
  77. package/src/generate/index.ts +0 -6
  78. package/src/github-ci/index.ts +49 -18
  79. package/src/github-ci/outputs.ts +1 -2
  80. package/src/lib/cli-package.ts +2 -2
  81. package/src/lib/json.ts +291 -32
  82. package/src/lib/workspace.ts +88 -100
  83. package/src/monorepo/__tests__/ci-workflow.test.ts +3 -3
  84. package/src/monorepo/ci-workflow.ts +27 -1
  85. package/src/monorepo/git-config.ts +2 -2
  86. package/src/monorepo/index.ts +6 -2
  87. package/src/monorepo/lockfile.test.ts +35 -36
  88. package/src/monorepo/lockfile.ts +48 -24
  89. package/src/monorepo/managed-files.test.ts +1 -0
  90. package/src/monorepo/managed-files.ts +57 -38
  91. package/src/monorepo/package-policy.test.ts +20 -50
  92. package/src/monorepo/package-policy.ts +76 -59
  93. package/src/monorepo/packed-manifest.ts +18 -52
  94. package/src/monorepo/packed-package.ts +43 -22
  95. package/src/monorepo/packs/index.ts +13 -4
  96. package/src/monorepo/publish-workflow.ts +3 -1
  97. package/src/monorepo/runtime.test.ts +42 -3
  98. package/src/monorepo/runtime.ts +90 -12
  99. package/src/monorepo/tool-validation.ts +35 -25
  100. package/src/monorepo/wrangler.test.ts +11 -15
  101. package/src/monorepo/wrangler.ts +15 -15
  102. package/src/nx/index.test.ts +9 -5
  103. package/src/nx/index.ts +69 -42
  104. package/src/pr/index.ts +42 -36
  105. package/src/release/__tests__/candidates.test.ts +5 -4
  106. package/src/release/__tests__/trust-publisher.test.ts +7 -0
  107. package/src/release/candidates.ts +10 -17
  108. package/src/release/index.ts +104 -73
  109. package/src/wrangler/prepare-env.ts +75 -54
  110. package/src/wrangler/scaffold.test.ts +9 -12
  111. package/src/wrangler/scaffold.ts +13 -13
@@ -21,15 +21,22 @@ import {
21
21
  checkWorkspaceConfigPolicy,
22
22
  } from '@smoothbricks/nx-plugin/workspace-config-policy';
23
23
  import {
24
- getOrCreateRecord,
25
- hasOwnString,
26
- isRecord,
24
+ ensureNx,
25
+ ensurePublishConfig,
26
+ ensureRepositoryObject,
27
+ ensureScripts,
28
+ type PackageExportMap,
29
+ type PackageExports,
30
+ type PackageJson,
27
31
  readJsonObject,
28
- recordProperty,
29
32
  requiredJsonObject,
30
- setMissingStringProperty,
33
+ type StringMap,
34
+ setMissingPackageStringField,
35
+ setMissingRepositoryField,
36
+ setNxName,
37
+ setPublishAccess,
38
+ setRepositoryField,
31
39
  setStringProperty,
32
- stringProperty,
33
40
  writeJsonObject,
34
41
  } from '../lib/json.js';
35
42
  import {
@@ -68,12 +75,12 @@ export function applyRootScriptDefaults(root: string): void {
68
75
  if (!rootPackage) {
69
76
  return;
70
77
  }
71
- const scripts = getOrCreateRecord(rootPackage, 'scripts');
78
+ const scripts = ensureScripts(rootPackage);
72
79
  let changed = false;
73
80
  for (const [name, command] of Object.entries(rootScriptPolicy)) {
74
81
  changed = setStringProperty(scripts, name, command) || changed;
75
82
  }
76
- const nx = getOrCreateRecord(rootPackage, 'nx');
83
+ const nx = ensureNx(rootPackage);
77
84
  if (!Array.isArray(nx.includedScripts) || nx.includedScripts.length !== 0) {
78
85
  nx.includedScripts = [];
79
86
  changed = true;
@@ -97,7 +104,7 @@ export function applyNxPluginDefaults(root: string): void {
97
104
 
98
105
  export function applyPublicPackageDefaults(root: string): void {
99
106
  const rootPackage = requiredJsonObject(join(root, 'package.json'));
100
- const rootLicense = stringProperty(rootPackage, 'license');
107
+ const rootLicense = rootPackage.license;
101
108
  const rootRepository = repositoryInfo(rootPackage);
102
109
 
103
110
  for (const pkg of listPublicPackages(root)) {
@@ -110,18 +117,21 @@ export function applyPublicPackageDefaults(root: string): void {
110
117
  rootLicense &&
111
118
  rootLicense !== 'UNLICENSED'
112
119
  ) {
113
- changed = setMissingStringProperty(pkg.json, 'license', rootLicense) || changed;
120
+ changed = setMissingPackageStringField(pkg.json, 'license', rootLicense) || changed;
114
121
  }
115
- const publishConfig = getOrCreateRecord(pkg.json, 'publishConfig');
116
- changed = setStringProperty(publishConfig, 'access', 'public') || changed;
117
-
118
- const repository = getOrCreateRecord(pkg.json, 'repository');
119
- changed =
120
- setStringProperty(repository, 'type', existingRepository?.type ?? rootRepository?.type ?? 'git') || changed;
121
- if (existingRepository && !stringProperty(repository, 'url')) {
122
- changed = setStringProperty(repository, 'url', existingRepository.url) || changed;
122
+ const publishConfig = ensurePublishConfig(pkg.json);
123
+ changed = setPublishAccess(publishConfig, 'public') || changed;
124
+
125
+ if (typeof pkg.json.repository !== 'string') {
126
+ const repository = ensureRepositoryObject(pkg.json);
127
+ changed =
128
+ setRepositoryField(repository, 'type', existingRepository?.type ?? rootRepository?.type ?? 'git') || changed;
129
+ if (existingRepository) {
130
+ changed = setMissingRepositoryField(repository, 'url', existingRepository.url) || changed;
131
+ }
132
+ changed = setRepositoryField(repository, 'directory', pkg.path.replaceAll('\\', '/')) || changed;
123
133
  }
124
- changed = setStringProperty(repository, 'directory', pkg.path.replaceAll('\\', '/')) || changed;
134
+
125
135
  changed = normalizeExportConditionOrder(pkg.json.exports) || changed;
126
136
  if (hasDevelopmentSourceExport(pkg.json.exports)) {
127
137
  changed = addFileEntry(pkg.json, 'src') || changed;
@@ -172,7 +182,7 @@ export function applyNxReleaseDefaults(root: string): void {
172
182
 
173
183
  export function applyNxProjectNameDefaults(root: string): void {
174
184
  const rootPackage = requiredJsonObject(join(root, 'package.json'));
175
- const rootName = stringProperty(rootPackage, 'name');
185
+ const rootName = rootPackage.name;
176
186
  if (!rootName) {
177
187
  return;
178
188
  }
@@ -181,8 +191,8 @@ export function applyNxProjectNameDefaults(root: string): void {
181
191
  if (!suggestedName) {
182
192
  continue;
183
193
  }
184
- const nx = getOrCreateRecord(pkg.json, 'nx');
185
- const changed = setStringProperty(nx, 'name', suggestedName);
194
+ const nx = ensureNx(pkg.json);
195
+ const changed = setNxName(nx, suggestedName);
186
196
  if (changed) {
187
197
  writeJsonObject(pkg.packageJsonPath, pkg.json);
188
198
  console.log(`updated ${pkg.path}/package.json nx.name`);
@@ -198,11 +208,10 @@ export function listValidCommitScopes(root: string): ReadonlySet<string> {
198
208
 
199
209
  export function listNxProjectNames(root: string): string[] {
200
210
  const rootPackage = readJsonObject(join(root, 'package.json'));
201
- const rootName = rootPackage ? stringProperty(rootPackage, 'name') : null;
211
+ const rootName = rootPackage?.name ?? null;
202
212
  const names: string[] = [];
203
213
  for (const pkg of getWorkspacePackageManifests(root)) {
204
- const nx = recordProperty(pkg.json, 'nx');
205
- const configuredName = nx ? stringProperty(nx, 'name') : null;
214
+ const configuredName = pkg.json.nx?.name;
206
215
  const suggestedName = rootName ? suggestNxProjectName(rootName, pkg.name) : null;
207
216
  if (configuredName) {
208
217
  names.push(configuredName);
@@ -220,11 +229,11 @@ export function validateRootPackagePolicy(root: string): number {
220
229
  return 1;
221
230
  }
222
231
  let failures = 0;
223
- if (!stringProperty(rootPackage, 'name')) {
232
+ if (!rootPackage.name) {
224
233
  console.error('package.json must define name');
225
234
  failures++;
226
235
  }
227
- if (!stringProperty(rootPackage, 'license')) {
236
+ if (!rootPackage.license) {
228
237
  console.error('package.json must define repo-wide license');
229
238
  failures++;
230
239
  }
@@ -234,19 +243,19 @@ export function validateRootPackagePolicy(root: string): number {
234
243
  }
235
244
  failures += validateRootScripts(rootPackage);
236
245
  failures += validateRootNxScriptInference(rootPackage);
237
- const packageManager = stringProperty(rootPackage, 'packageManager');
246
+ const packageManager = rootPackage.packageManager;
238
247
  if (!packageManager?.startsWith('bun@')) {
239
248
  console.error('package.json packageManager must use bun@<version>');
240
249
  failures++;
241
250
  }
242
251
  const bunVersion = packageManager?.startsWith('bun@') ? packageManager.slice('bun@'.length) : null;
243
- const devDependencies = recordProperty(rootPackage, 'devDependencies');
252
+ const devDependencies = rootPackage.devDependencies;
244
253
  if (!bunVersion || !devDependencies || devDependencies['@types/bun'] !== bunVersion) {
245
254
  console.error('package.json devDependencies.@types/bun must match packageManager bun version');
246
255
  failures++;
247
256
  }
248
- const engines = recordProperty(rootPackage, 'engines');
249
- if (!engines || !stringProperty(engines, 'node')) {
257
+ const engines = rootPackage.engines;
258
+ if (!engines?.node) {
250
259
  console.error('package.json engines.node must be defined');
251
260
  failures++;
252
261
  }
@@ -268,7 +277,7 @@ export function validateNxReleaseConfig(root: string): number {
268
277
 
269
278
  export function validateNxProjectNames(root: string): number {
270
279
  const rootPackage = readJsonObject(join(root, 'package.json'));
271
- const rootName = rootPackage ? stringProperty(rootPackage, 'name') : null;
280
+ const rootName = rootPackage?.name ?? null;
272
281
  if (!rootName) {
273
282
  return 0;
274
283
  }
@@ -278,8 +287,7 @@ export function validateNxProjectNames(root: string): number {
278
287
  if (!suggestedName) {
279
288
  continue;
280
289
  }
281
- const nx = recordProperty(pkg.json, 'nx');
282
- const configuredName = nx ? stringProperty(nx, 'name') : null;
290
+ const configuredName = pkg.json.nx?.name ?? null;
283
291
  if (configuredName !== suggestedName) {
284
292
  console.error(
285
293
  `${pkg.path}: package.json nx.name must be "${suggestedName}" so fix(${suggestedName}): maps to this project`,
@@ -318,16 +326,19 @@ export function validatePublicPackageMetadata(root: string): number {
318
326
  console.error(`${pkg.path}: npm:public package must not be private`);
319
327
  failures++;
320
328
  }
321
- if (!stringProperty(pkg.json, 'license')) {
329
+ if (!pkg.json.license) {
322
330
  console.error(`${pkg.path}: public package must define license`);
323
331
  failures++;
324
332
  }
325
- const publishConfig = recordProperty(pkg.json, 'publishConfig');
326
- if (!publishConfig || stringProperty(publishConfig, 'access') !== 'public') {
333
+ const publishConfig = pkg.json.publishConfig;
334
+ if (publishConfig?.access !== 'public') {
327
335
  console.error(`${pkg.path}: public package must define publishConfig.access = public`);
328
336
  failures++;
329
337
  }
330
- const repository = recordProperty(pkg.json, 'repository');
338
+ const repository =
339
+ pkg.json.repository !== null && pkg.json.repository !== undefined && typeof pkg.json.repository !== 'string'
340
+ ? pkg.json.repository
341
+ : null;
331
342
  const packageRepository = packageRepositoryInfo(pkg);
332
343
  if (!packageRepository) {
333
344
  console.error(`${pkg.path}: public package must define repository.url`);
@@ -345,11 +356,11 @@ export function validatePublicPackageMetadata(root: string): number {
345
356
  );
346
357
  failures++;
347
358
  }
348
- if (!repository || !stringProperty(repository, 'type')) {
359
+ if (!repository?.type) {
349
360
  console.error(`${pkg.path}: public package must define repository.type`);
350
361
  failures++;
351
362
  }
352
- if (!repository || stringProperty(repository, 'directory') !== pkg.path.replaceAll('\\', '/')) {
363
+ if (!repository || repository.directory !== pkg.path.replaceAll('\\', '/')) {
353
364
  console.error(`${pkg.path}: public package repository.directory must be ${pkg.path.replaceAll('\\', '/')}`);
354
365
  failures++;
355
366
  }
@@ -357,11 +368,14 @@ export function validatePublicPackageMetadata(root: string): number {
357
368
  console.error(`${pkg.path}: public package must define files`);
358
369
  failures++;
359
370
  }
360
- if (!isRecord(pkg.json.exports) && !isRecord(pkg.json.bin)) {
371
+ const hasBin =
372
+ typeof pkg.json.bin === 'string' ||
373
+ (pkg.json.bin !== null && pkg.json.bin !== undefined && typeof pkg.json.bin === 'object');
374
+ if (!isPackageExportMap(pkg.json.exports) && !hasBin) {
361
375
  console.error(`${pkg.path}: public package must define exports or bin`);
362
376
  failures++;
363
377
  }
364
- if (!hasOwnString(pkg.json, 'types') && !isRecord(pkg.json.bin)) {
378
+ if (typeof pkg.json.types !== 'string' && !hasBin) {
365
379
  console.error(`${pkg.path}: public library package must define types`);
366
380
  failures++;
367
381
  }
@@ -375,7 +389,7 @@ export function validateWorkspaceDependencies(root: string, options: PackageTarg
375
389
  const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
376
390
  for (const pkg of listPackageJsonRecords(root)) {
377
391
  for (const field of workspaceDependencyFields) {
378
- const dependencies = recordProperty(pkg.json, field);
392
+ const dependencies = pkg.json[field];
379
393
  if (!dependencies) {
380
394
  continue;
381
395
  }
@@ -436,10 +450,10 @@ function validateCiSkipTags(root: string, options: PackageTargetPolicyOptions):
436
450
  // Helpers kept in CLI (not Nx-specific)
437
451
  // ---------------------------------------------------------------------------
438
452
 
439
- function fixWorkspaceDependencyRanges(pkg: Record<string, unknown>, workspaceNames: Set<string>): boolean {
453
+ function fixWorkspaceDependencyRanges(pkg: PackageJson, workspaceNames: Set<string>): boolean {
440
454
  let changed = false;
441
455
  for (const field of workspaceDependencyFields) {
442
- const dependencies = recordProperty(pkg, field);
456
+ const dependencies = pkg[field];
443
457
  if (!dependencies) {
444
458
  continue;
445
459
  }
@@ -453,8 +467,8 @@ function fixWorkspaceDependencyRanges(pkg: Record<string, unknown>, workspaceNam
453
467
  return changed;
454
468
  }
455
469
 
456
- function validateRootScripts(rootPackage: Record<string, unknown>): number {
457
- const scripts = recordProperty(rootPackage, 'scripts');
470
+ function validateRootScripts(rootPackage: PackageJson): number {
471
+ const scripts = rootPackage.scripts;
458
472
  let failures = 0;
459
473
  for (const [name, command] of Object.entries(rootScriptPolicy)) {
460
474
  if (scripts?.[name] !== command) {
@@ -471,9 +485,9 @@ function validateRootScripts(rootPackage: Record<string, unknown>): number {
471
485
  return failures;
472
486
  }
473
487
 
474
- function validateRootNxScriptInference(rootPackage: Record<string, unknown>): number {
475
- const nx = recordProperty(rootPackage, 'nx');
476
- if (nx && Array.isArray(nx.includedScripts) && nx.includedScripts.length === 0) {
488
+ function validateRootNxScriptInference(rootPackage: PackageJson): number {
489
+ const includedScripts = rootPackage.nx?.includedScripts;
490
+ if (Array.isArray(includedScripts) && includedScripts.length === 0) {
477
491
  return 0;
478
492
  }
479
493
  console.error('package.json nx.includedScripts must be [] so root scripts do not become recursive Nx targets.');
@@ -497,12 +511,12 @@ function unscopedPackageName(packageName: string): string {
497
511
  return packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
498
512
  }
499
513
 
500
- function recordKeysAreSorted(record: Record<string, unknown>): boolean {
514
+ function recordKeysAreSorted(record: StringMap): boolean {
501
515
  const keys = Object.keys(record);
502
- return keys.every((key, index) => index === 0 || keys[index - 1] <= key);
516
+ return keys.every((key, index) => index === 0 || (keys[index - 1] ?? '') <= key);
503
517
  }
504
518
 
505
- function sortRecordInPlace(record: Record<string, unknown>): boolean {
519
+ function sortRecordInPlace(record: StringMap): boolean {
506
520
  if (recordKeysAreSorted(record)) {
507
521
  return false;
508
522
  }
@@ -516,8 +530,11 @@ function sortRecordInPlace(record: Record<string, unknown>): boolean {
516
530
  return true;
517
531
  }
518
532
 
519
- function normalizeExportConditionOrder(value: unknown): boolean {
520
- if (!isRecord(value)) {
533
+ function isPackageExportMap(value: PackageExports): value is PackageExportMap {
534
+ return value !== null && value !== undefined && typeof value === 'object';
535
+ }
536
+ function normalizeExportConditionOrder(value: PackageExports): boolean {
537
+ if (!isPackageExportMap(value)) {
521
538
  return false;
522
539
  }
523
540
  let changed = false;
@@ -546,8 +563,8 @@ function normalizeExportConditionOrder(value: unknown): boolean {
546
563
  return true;
547
564
  }
548
565
 
549
- function hasDevelopmentSourceExport(value: unknown): boolean {
550
- if (!isRecord(value)) {
566
+ function hasDevelopmentSourceExport(value: PackageExports): boolean {
567
+ if (!isPackageExportMap(value)) {
551
568
  return false;
552
569
  }
553
570
  for (const [key, child] of Object.entries(value)) {
@@ -561,12 +578,12 @@ function hasDevelopmentSourceExport(value: unknown): boolean {
561
578
  return false;
562
579
  }
563
580
 
564
- function addFileEntry(pkg: Record<string, unknown>, entry: string): boolean {
581
+ function addFileEntry(pkg: PackageJson, entry: string): boolean {
565
582
  const files = pkg.files;
566
583
  if (!Array.isArray(files) || files.includes(entry)) {
567
584
  return false;
568
585
  }
569
- const firstNegated = files.findIndex((file) => typeof file === 'string' && file.startsWith('!'));
586
+ const firstNegated = files.findIndex((file) => file.startsWith('!'));
570
587
  if (firstNegated === -1) {
571
588
  files.push(entry);
572
589
  } else {
@@ -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 }> {
@@ -3,7 +3,7 @@ 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
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) +
@@ -420,7 +420,9 @@ function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDef
420
420
  ' "$GITHUB_OUTPUT"',
421
421
  ];
422
422
  case PublishWorkflowStepKind.CheckManagedMonorepoFiles:
423
- return conditionalRunStep(step, 'smoo monorepo check');
423
+ // Drift is derived state with its own remediation PR; publishing only
424
+ // blocks on actual package issues (smoo monorepo validate).
425
+ return conditionalRunStep(step, 'smoo monorepo check --warn');
424
426
  case PublishWorkflowStepKind.Build:
425
427
  return conditionalRunStep(
426
428
  step,