@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
@@ -29,7 +29,6 @@ const buildOutputDependencies = [
29
29
  '*-web',
30
30
  '*-html',
31
31
  '*-css',
32
- '*-ios',
33
32
  '*-android',
34
33
  '*-native',
35
34
  '*-napi',
@@ -263,7 +262,7 @@ describe('workspace package script policy', () => {
263
262
  try {
264
263
  expect(validateWorkspaceDependencies(root)).toBe(2);
265
264
 
266
- const resolvedTargetsByProject = new Map([['native', new Set(['build', 'tsc-js', 'zig-wasm'])]]);
265
+ const resolvedTargetsByProject = new Map([['native', new Set(['build', 'tsc-js', 'tsdown-js'])]]);
267
266
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
268
267
 
269
268
  const native = await readJson(join(root, 'packages/native/package.json'));
@@ -285,17 +284,17 @@ describe('workspace package script policy', () => {
285
284
  dependencies: { '@smoothbricks/lib': 'workspace:*' },
286
285
  scripts: {
287
286
  'build:custom': 'nx run native:build:custom',
288
- 'build:zig': 'nx run native:build:zig',
287
+ 'build:bundle': 'nx run native:build:bundle',
289
288
  'build:ts': 'nx run native:build:ts',
290
289
  },
291
290
  nx: {
292
291
  name: 'native',
293
292
  targets: {
294
- build: { dependsOn: ['^build', 'build:ts', 'build:zig', 'build:custom'] },
293
+ build: { dependsOn: ['^build', 'build:ts', 'build:bundle', 'build:custom'] },
295
294
  'build:custom': {},
296
- 'build:zig': {
295
+ 'build:bundle': {
297
296
  executor: 'nx:run-commands',
298
- options: { command: 'zig build wasm', cwd: '{projectRoot}' },
297
+ options: { command: 'tsdown', cwd: '{projectRoot}' },
299
298
  },
300
299
  'build:ts': {
301
300
  executor: 'nx:run-commands',
@@ -308,25 +307,21 @@ describe('workspace package script policy', () => {
308
307
  });
309
308
  try {
310
309
  await writeJson(join(root, 'packages/native/tsconfig.lib.json'), {});
311
- await writeFile(
312
- join(root, 'packages/native/build.zig'),
313
- 'pub fn build(b: *std.Build) void { _ = b.step("wasm", "Build wasm"); }\n',
314
- );
315
310
  expect(validateWorkspaceDependencies(root)).toBe(6);
316
311
 
317
- const resolvedTargetsByProject = new Map([['native', new Set(['build', 'custom', 'tsc-js', 'zig-wasm'])]]);
312
+ const resolvedTargetsByProject = new Map([['native', new Set(['build', 'custom', 'tsc-js', 'tsdown-js'])]]);
318
313
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
319
314
 
320
315
  const native = await readJson(join(root, 'packages/native/package.json'));
321
316
  expect(native.scripts).toEqual({
322
317
  'build:custom': 'nx run native:custom',
323
- 'build:zig': 'nx run native:zig-wasm',
318
+ 'build:bundle': 'nx run native:tsdown-js',
324
319
  'build:ts': 'nx run native:tsc-js',
325
320
  });
326
321
  expect(native.nx).toEqual({
327
322
  name: 'native',
328
323
  targets: {
329
- build: { dependsOn: ['^build', 'tsc-js', 'zig-wasm', 'custom'] },
324
+ build: { dependsOn: ['^build', 'tsc-js', 'tsdown-js', 'custom'] },
330
325
  },
331
326
  });
332
327
  expect(validateWorkspaceDependencies(root, { resolvedTargetsByProject })).toBe(0);
@@ -345,7 +340,7 @@ describe('workspace package script policy', () => {
345
340
  nx: {
346
341
  name: 'native',
347
342
  targets: {
348
- build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'zig-wasm', 'custom'] },
343
+ build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'tsdown-js', 'custom'] },
349
344
  },
350
345
  },
351
346
  },
@@ -353,14 +348,10 @@ describe('workspace package script policy', () => {
353
348
  });
354
349
  try {
355
350
  await writeJson(join(root, 'packages/native/tsconfig.lib.json'), {});
356
- await writeFile(
357
- join(root, 'packages/native/build.zig'),
358
- 'pub fn build(b: *std.Build) void { _ = b.step("wasm", "Build wasm"); }\n',
359
- );
360
351
  const resolvedTargetsByProject = new Map([
361
352
  [
362
353
  'native',
363
- { targets: new Set(['build', 'tsc-js', 'zig-wasm']), buildDependsOn: ['^build', 'tsc-js', 'zig-wasm'] },
354
+ { targets: new Set(['build', 'tsc-js', 'tsdown-js']), buildDependsOn: ['^build', 'tsc-js', 'tsdown-js'] },
364
355
  ],
365
356
  ]);
366
357
 
@@ -370,7 +361,7 @@ describe('workspace package script policy', () => {
370
361
  expect(native.nx).toEqual({
371
362
  name: 'native',
372
363
  targets: {
373
- build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'zig-wasm', 'custom'] },
364
+ build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'tsdown-js', 'custom'] },
374
365
  },
375
366
  });
376
367
  } finally {
@@ -390,8 +381,8 @@ describe('workspace package script policy', () => {
390
381
  targets: {
391
382
  build: {
392
383
  executor: 'nx:run-commands',
393
- options: { command: 'zig build wasm', cwd: '{projectRoot}' },
394
- dependsOn: ['^build', 'zig-wasm'],
384
+ options: { command: 'tsdown', cwd: '{projectRoot}' },
385
+ dependsOn: ['^build', 'tsdown-js'],
395
386
  },
396
387
  },
397
388
  },
@@ -399,12 +390,8 @@ describe('workspace package script policy', () => {
399
390
  ],
400
391
  });
401
392
  try {
402
- await writeFile(
403
- join(root, 'packages/native/build.zig'),
404
- 'pub fn build(b: *std.Build) void { _ = b.step("wasm", "Build wasm"); }\n',
405
- );
406
393
  const resolvedTargetsByProject = new Map([
407
- ['native', { targets: new Set(['build', 'zig-wasm']), buildDependsOn: ['^build', 'zig-wasm'] }],
394
+ ['native', { targets: new Set(['build', 'tsdown-js']), buildDependsOn: ['^build', 'tsdown-js'] }],
408
395
  ]);
409
396
 
410
397
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
@@ -415,8 +402,8 @@ describe('workspace package script policy', () => {
415
402
  targets: {
416
403
  build: {
417
404
  executor: 'nx:run-commands',
418
- options: { command: 'zig build wasm', cwd: '{projectRoot}' },
419
- dependsOn: ['^build', 'zig-wasm'],
405
+ options: { command: 'tsdown', cwd: '{projectRoot}' },
406
+ dependsOn: ['^build', 'tsdown-js'],
420
407
  },
421
408
  },
422
409
  });
@@ -435,7 +422,7 @@ describe('workspace package script policy', () => {
435
422
  nx: {
436
423
  name: 'native',
437
424
  targets: {
438
- build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'zig-wasm'] },
425
+ build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'tsdown-js'] },
439
426
  },
440
427
  },
441
428
  },
@@ -445,7 +432,7 @@ describe('workspace package script policy', () => {
445
432
  const resolvedTargetsByProject = new Map([
446
433
  [
447
434
  'native',
448
- { targets: new Set(['build', 'tsc-js', 'zig-wasm']), buildDependsOn: ['^build', 'tsc-js', 'zig-wasm'] },
435
+ { targets: new Set(['build', 'tsc-js', 'tsdown-js']), buildDependsOn: ['^build', 'tsc-js', 'tsdown-js'] },
449
436
  ],
450
437
  ]);
451
438
 
@@ -499,7 +486,7 @@ describe('workspace package script policy', () => {
499
486
  });
500
487
  try {
501
488
  const resolvedTargetsByProject = new Map([
502
- ['native', { targets: new Set(['build', 'tsc-js', 'zig-wasm']), buildDependsOn: buildOutputDependencies }],
489
+ ['native', { targets: new Set(['build', 'tsc-js', 'tsdown-js']), buildDependsOn: buildOutputDependencies }],
503
490
  ]);
504
491
 
505
492
  applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
@@ -559,20 +546,6 @@ describe('workspace package script policy', () => {
559
546
  }
560
547
  });
561
548
 
562
- it('rejects build.zig without steps', async () => {
563
- const root = await createWorkspace({
564
- rootName: '@smoothbricks/codebase',
565
- packages: [{ dir: 'native', name: '@smoothbricks/native', nx: { name: 'native' } }],
566
- });
567
- try {
568
- await writeFile(join(root, 'packages/native/build.zig'), 'pub fn build(b: *std.Build) void { _ = b; }\n');
569
-
570
- expect(validateWorkspaceDependencies(root)).toBe(1);
571
- } finally {
572
- await rm(root, { recursive: true, force: true });
573
- }
574
- });
575
-
576
549
  it('rejects old test tsconfig output for packages with test runners', async () => {
577
550
  const root = await createWorkspace({
578
551
  rootName: '@smoothbricks/codebase',
@@ -729,17 +702,13 @@ describe('workspace package script policy', () => {
729
702
  }
730
703
  });
731
704
 
732
- it('accepts noEmit test tsconfig and build.zig with an explicit step', async () => {
705
+ it('accepts a noEmit test tsconfig', async () => {
733
706
  const root = await createWorkspace({
734
707
  rootName: '@smoothbricks/codebase',
735
708
  packages: [{ dir: 'native', name: '@smoothbricks/native', nx: { name: 'native', targets: { lint: {} } } }],
736
709
  });
737
710
  try {
738
711
  await writeJson(join(root, 'packages/native/tsconfig.test.json'), { compilerOptions: { noEmit: true } });
739
- await writeFile(
740
- join(root, 'packages/native/build.zig'),
741
- 'pub fn build(b: *std.Build) void { _ = b.step("build", "Build native artifact"); }\n',
742
- );
743
712
 
744
713
  expect(validateWorkspaceDependencies(root)).toBe(0);
745
714
  } finally {
@@ -974,6 +943,56 @@ describe('workspace package script policy', () => {
974
943
  await rm(root, { recursive: true, force: true });
975
944
  }
976
945
  });
946
+ it('validates generic CI skip tags against resolved project targets', async () => {
947
+ const root = await createWorkspace({
948
+ rootName: '@fixture/root',
949
+ packages: [
950
+ {
951
+ dir: 'worker',
952
+ name: '@fixture/worker',
953
+ nx: { name: 'worker', tags: ['ci:skip:test', 'ci:skip:test.integration'], targets: {} },
954
+ },
955
+ ],
956
+ });
957
+ try {
958
+ const resolvedTargetsByProject = new Map([['worker', new Set(['build', 'test', 'test.integration'])]]);
959
+ expect(validateWorkspaceDependencies(root, { resolvedTargetsByProject })).toBe(0);
960
+ } finally {
961
+ await rm(root, { recursive: true, force: true });
962
+ }
963
+ });
964
+
965
+ it('rejects malformed and missing-target CI skip tags', async () => {
966
+ const root = await createWorkspace({
967
+ rootName: '@fixture/root',
968
+ packages: [
969
+ {
970
+ dir: 'malformed',
971
+ name: '@fixture/malformed',
972
+ nx: { name: 'malformed', tags: ['ci:skip:'], targets: {} },
973
+ },
974
+ {
975
+ dir: 'missing',
976
+ name: '@fixture/missing',
977
+ nx: { name: 'missing', tags: ['ci:skip:test'], targets: {} },
978
+ },
979
+ ],
980
+ });
981
+ const errors = captureConsoleErrors();
982
+ try {
983
+ const resolvedTargetsByProject = new Map([
984
+ ['malformed', new Set(['test'])],
985
+ ['missing', new Set(['build'])],
986
+ ]);
987
+ expect(validateWorkspaceDependencies(root, { resolvedTargetsByProject })).toBe(2);
988
+ expect(errors).toContain(
989
+ 'packages/malformed: nx tag ci:skip: must use ci:skip:<target> with one exact Nx target name',
990
+ );
991
+ expect(errors).toContain('packages/missing: nx tag ci:skip:test names missing Nx target test');
992
+ } finally {
993
+ await rm(root, { recursive: true, force: true });
994
+ }
995
+ });
977
996
  });
978
997
 
979
998
  async function createWorkspace(input: {
@@ -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
  }
@@ -371,10 +385,11 @@ export function validatePublicPackageMetadata(root: string): number {
371
385
 
372
386
  export function validateWorkspaceDependencies(root: string, options: PackageTargetPolicyOptions = {}): number {
373
387
  let failures = 0;
388
+ failures += validateCiSkipTags(root, options);
374
389
  const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
375
390
  for (const pkg of listPackageJsonRecords(root)) {
376
391
  for (const field of workspaceDependencyFields) {
377
- const dependencies = recordProperty(pkg.json, field);
392
+ const dependencies = pkg.json[field];
378
393
  if (!dependencies) {
379
394
  continue;
380
395
  }
@@ -404,14 +419,41 @@ export function validateWorkspaceDependencies(root: string, options: PackageTarg
404
419
  return failures;
405
420
  }
406
421
 
422
+ const ciSkipTagPrefix = 'ci:skip:';
423
+ const invalidCiSkipTargetSyntax = /[,*?[\]{}]/;
424
+
425
+ function validateCiSkipTags(root: string, options: PackageTargetPolicyOptions): number {
426
+ let failures = 0;
427
+ for (const pkg of getWorkspacePackages(root)) {
428
+ const resolved = options.resolvedTargetsByProject?.get(pkg.projectName);
429
+ const resolvedTargets = resolved && 'targets' in resolved ? resolved.targets : resolved;
430
+ for (const tag of pkg.tags) {
431
+ if (tag !== 'ci:skip' && !tag.startsWith(ciSkipTagPrefix)) {
432
+ continue;
433
+ }
434
+ const target = tag.startsWith(ciSkipTagPrefix) ? tag.slice(ciSkipTagPrefix.length) : '';
435
+ if (!target || invalidCiSkipTargetSyntax.test(target)) {
436
+ console.error(`${pkg.path}: nx tag ${tag} must use ci:skip:<target> with one exact Nx target name`);
437
+ failures++;
438
+ continue;
439
+ }
440
+ if (resolvedTargets && !resolvedTargets.has(target)) {
441
+ console.error(`${pkg.path}: nx tag ${tag} names missing Nx target ${target}`);
442
+ failures++;
443
+ }
444
+ }
445
+ }
446
+ return failures;
447
+ }
448
+
407
449
  // ---------------------------------------------------------------------------
408
450
  // Helpers kept in CLI (not Nx-specific)
409
451
  // ---------------------------------------------------------------------------
410
452
 
411
- function fixWorkspaceDependencyRanges(pkg: Record<string, unknown>, workspaceNames: Set<string>): boolean {
453
+ function fixWorkspaceDependencyRanges(pkg: PackageJson, workspaceNames: Set<string>): boolean {
412
454
  let changed = false;
413
455
  for (const field of workspaceDependencyFields) {
414
- const dependencies = recordProperty(pkg, field);
456
+ const dependencies = pkg[field];
415
457
  if (!dependencies) {
416
458
  continue;
417
459
  }
@@ -425,8 +467,8 @@ function fixWorkspaceDependencyRanges(pkg: Record<string, unknown>, workspaceNam
425
467
  return changed;
426
468
  }
427
469
 
428
- function validateRootScripts(rootPackage: Record<string, unknown>): number {
429
- const scripts = recordProperty(rootPackage, 'scripts');
470
+ function validateRootScripts(rootPackage: PackageJson): number {
471
+ const scripts = rootPackage.scripts;
430
472
  let failures = 0;
431
473
  for (const [name, command] of Object.entries(rootScriptPolicy)) {
432
474
  if (scripts?.[name] !== command) {
@@ -443,9 +485,9 @@ function validateRootScripts(rootPackage: Record<string, unknown>): number {
443
485
  return failures;
444
486
  }
445
487
 
446
- function validateRootNxScriptInference(rootPackage: Record<string, unknown>): number {
447
- const nx = recordProperty(rootPackage, 'nx');
448
- 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) {
449
491
  return 0;
450
492
  }
451
493
  console.error('package.json nx.includedScripts must be [] so root scripts do not become recursive Nx targets.');
@@ -469,12 +511,12 @@ function unscopedPackageName(packageName: string): string {
469
511
  return packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
470
512
  }
471
513
 
472
- function recordKeysAreSorted(record: Record<string, unknown>): boolean {
514
+ function recordKeysAreSorted(record: StringMap): boolean {
473
515
  const keys = Object.keys(record);
474
- return keys.every((key, index) => index === 0 || keys[index - 1] <= key);
516
+ return keys.every((key, index) => index === 0 || (keys[index - 1] ?? '') <= key);
475
517
  }
476
518
 
477
- function sortRecordInPlace(record: Record<string, unknown>): boolean {
519
+ function sortRecordInPlace(record: StringMap): boolean {
478
520
  if (recordKeysAreSorted(record)) {
479
521
  return false;
480
522
  }
@@ -488,8 +530,11 @@ function sortRecordInPlace(record: Record<string, unknown>): boolean {
488
530
  return true;
489
531
  }
490
532
 
491
- function normalizeExportConditionOrder(value: unknown): boolean {
492
- 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)) {
493
538
  return false;
494
539
  }
495
540
  let changed = false;
@@ -518,8 +563,8 @@ function normalizeExportConditionOrder(value: unknown): boolean {
518
563
  return true;
519
564
  }
520
565
 
521
- function hasDevelopmentSourceExport(value: unknown): boolean {
522
- if (!isRecord(value)) {
566
+ function hasDevelopmentSourceExport(value: PackageExports): boolean {
567
+ if (!isPackageExportMap(value)) {
523
568
  return false;
524
569
  }
525
570
  for (const [key, child] of Object.entries(value)) {
@@ -533,12 +578,12 @@ function hasDevelopmentSourceExport(value: unknown): boolean {
533
578
  return false;
534
579
  }
535
580
 
536
- function addFileEntry(pkg: Record<string, unknown>, entry: string): boolean {
581
+ function addFileEntry(pkg: PackageJson, entry: string): boolean {
537
582
  const files = pkg.files;
538
583
  if (!Array.isArray(files) || files.includes(entry)) {
539
584
  return false;
540
585
  }
541
- const firstNegated = files.findIndex((file) => typeof file === 'string' && file.startsWith('!'));
586
+ const firstNegated = files.findIndex((file) => file.startsWith('!'));
542
587
  if (firstNegated === -1) {
543
588
  files.push(entry);
544
589
  } else {