@smoothbricks/cli 0.10.5 → 0.10.7

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 (65) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/github-ci/index.d.ts +1 -0
  3. package/dist/github-ci/index.d.ts.map +1 -1
  4. package/dist/github-ci/index.js +28 -5
  5. package/dist/lib/json.d.ts +3 -1
  6. package/dist/lib/json.d.ts.map +1 -1
  7. package/dist/lib/json.js +5 -20
  8. package/dist/lib/workspace.d.ts.map +1 -1
  9. package/dist/lib/workspace.js +1 -0
  10. package/dist/monorepo/ci-workflow.d.ts +2 -0
  11. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  12. package/dist/monorepo/ci-workflow.js +10 -4
  13. package/dist/monorepo/github-runs-on.d.ts +14 -0
  14. package/dist/monorepo/github-runs-on.d.ts.map +1 -0
  15. package/dist/monorepo/github-runs-on.js +43 -0
  16. package/dist/monorepo/index.d.ts +2 -2
  17. package/dist/monorepo/index.d.ts.map +1 -1
  18. package/dist/monorepo/index.js +7 -7
  19. package/dist/monorepo/managed-files.d.ts +14 -3
  20. package/dist/monorepo/managed-files.d.ts.map +1 -1
  21. package/dist/monorepo/managed-files.js +54 -74
  22. package/dist/monorepo/package-policy.js +2 -2
  23. package/dist/monorepo/packs/index.d.ts.map +1 -1
  24. package/dist/monorepo/packs/index.js +3 -1
  25. package/dist/monorepo/publish-workflow.d.ts +3 -0
  26. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  27. package/dist/monorepo/publish-workflow.js +17 -14
  28. package/dist/monorepo/runtime.d.ts +2 -2
  29. package/dist/monorepo/runtime.d.ts.map +1 -1
  30. package/dist/monorepo/runtime.js +11 -16
  31. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  32. package/dist/monorepo/tool-validation.js +20 -23
  33. package/dist/nx/index.d.ts +7 -4
  34. package/dist/nx/index.d.ts.map +1 -1
  35. package/dist/nx/index.js +117 -56
  36. package/managed/raw/tooling/devenv +71 -0
  37. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +24 -7
  38. package/managed/templates/github/actions/cache-nix-devenv/action.yml +13 -11
  39. package/managed/templates/github/actions/save-nix-devenv/action.yml +34 -3
  40. package/managed/templates/github/actions/setup-devenv/action.yml +44 -15
  41. package/managed/templates/github/workflows/managed-files.yml +64 -12
  42. package/package.json +4 -4
  43. package/src/cli.ts +10 -3
  44. package/src/github-ci/index.test.ts +53 -4
  45. package/src/github-ci/index.ts +37 -5
  46. package/src/lib/json.ts +3 -1
  47. package/src/lib/workspace.ts +1 -0
  48. package/src/monorepo/__tests__/ci-workflow.test.ts +37 -1
  49. package/src/monorepo/__tests__/publish-workflow.test.ts +39 -4
  50. package/src/monorepo/ci-workflow.ts +14 -4
  51. package/src/monorepo/github-runs-on.ts +45 -0
  52. package/src/monorepo/index.ts +7 -7
  53. package/src/monorepo/managed-files.test.ts +58 -0
  54. package/src/monorepo/managed-files.ts +59 -76
  55. package/src/monorepo/package-policy.test.ts +31 -0
  56. package/src/monorepo/package-policy.ts +2 -2
  57. package/src/monorepo/packs/index.ts +3 -1
  58. package/src/monorepo/publish-workflow.ts +28 -15
  59. package/src/monorepo/runtime.test.ts +23 -17
  60. package/src/monorepo/runtime.ts +12 -17
  61. package/src/monorepo/tool-validation.test.ts +15 -8
  62. package/src/monorepo/tool-validation.ts +20 -25
  63. package/src/nx/index.test.ts +45 -14
  64. package/src/nx/index.ts +133 -64
  65. package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
@@ -7,6 +7,7 @@ import {
7
7
  } from '@smoothbricks/nx-plugin/workspace-config-policy';
8
8
  import type { Options as PrettierOptions } from 'prettier';
9
9
  import { isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
10
+ import { renderRunsOnLine, type WorkflowRunsOn } from './github-runs-on.js';
10
11
 
11
12
  const PUBLISH_WORKFLOW_FORMAT_OPTIONS = Object.freeze({
12
13
  parser: 'yaml',
@@ -55,6 +56,8 @@ export interface PublishWorkflowDefinitionOptions {
55
56
  deployProvider?: 'cloudflare';
56
57
  repoName?: string;
57
58
  platformTargetGlobs?: readonly string[];
59
+ /** Linux jobs only. Default ubuntu-latest. Same smoo.github.runsOn as CI. */
60
+ runsOn?: WorkflowRunsOn;
58
61
  }
59
62
 
60
63
  export interface PublishWorkflowInputs {
@@ -441,7 +444,8 @@ function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDef
441
444
  case PublishWorkflowStepKind.UploadTraceDbs:
442
445
  return [
443
446
  ` - name: ${step.name}`,
444
- ` if: ${githubExpression('failure()')}`,
447
+ ' # Default is success(); failure() uploads traces only when the job already failed.',
448
+ ' if: failure()',
445
449
  ' uses: actions/upload-artifact@v7.0.1',
446
450
  ' with:',
447
451
  ` name: trace-results-${githubExpression('github.run_id')}`,
@@ -465,7 +469,8 @@ function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDef
465
469
  case PublishWorkflowStepKind.SaveNixDevenv:
466
470
  return [
467
471
  ` - name: ${step.name}`,
468
- ` if: ${githubExpression('always()')}`,
472
+ ' # success() is default; always() still saves GH Nix cache after a red job.',
473
+ ' if: always()',
469
474
  ' uses: ./.github/actions/save-nix-devenv',
470
475
  ' with:',
471
476
  ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
@@ -498,7 +503,7 @@ function deployEnvLines(options: PublishWorkflowDefinitionOptions): string[] {
498
503
 
499
504
  function conditionalRunStep(step: PublishWorkflowStep, run: string): string[] {
500
505
  const condition = "steps.version.outputs.mode != 'none'";
501
- return [` - name: ${step.name}`, ` if: ${githubExpression(condition)}`, ` run: ${run}`];
506
+ return [` - name: ${step.name}`, ` if: ${condition}`, ` run: ${run}`];
502
507
  }
503
508
 
504
509
  function renderSingleJobPublishWorkflowSteps(
@@ -514,7 +519,7 @@ function renderSingleJobPublishWorkflowSteps(
514
519
  if (step.kind === PublishWorkflowStepKind.Build) {
515
520
  lines.push(
516
521
  ' - name: 🐧 Build supplemental Linux targets',
517
- ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
522
+ " if: steps.version.outputs.mode != 'none'",
518
523
  ` run: smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects "${githubExpression(
519
524
  'steps.version.outputs.projects',
520
525
  )}"`,
@@ -566,7 +571,7 @@ defaults:
566
571
 
567
572
  jobs:
568
573
  linux-release-candidate:
569
- runs-on: ubuntu-latest
574
+ ${renderRunsOnLine(options.runsOn)}
570
575
  permissions:
571
576
  contents: write
572
577
  id-token: none
@@ -642,7 +647,7 @@ function renderLinuxReleaseCandidateSteps(
642
647
  } else if (step.kind === PublishWorkflowStepKind.Build) {
643
648
  lines.push(
644
649
  ` - name: ${step.name}`,
645
- ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
650
+ " if: steps.version.outputs.mode != 'none'",
646
651
  ' run:',
647
652
  ` smoo github-ci nx-run-many --targets build --projects "${githubExpression(
648
653
  'steps.version.outputs.projects',
@@ -667,7 +672,7 @@ function renderLinuxReleaseCandidateSteps(
667
672
  '',
668
673
  ` # Step ${stepNumber}`,
669
674
  ' - name: 🐧 Build supplemental Linux targets',
670
- ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
675
+ " if: steps.version.outputs.mode != 'none'",
671
676
  ' run:',
672
677
  ` smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(
673
678
  ',',
@@ -700,7 +705,7 @@ function renderLinuxReleaseCandidateSteps(
700
705
  '',
701
706
  ` # Step ${stepNumber++}`,
702
707
  ' - name: 📤 Upload validated build outputs',
703
- ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
708
+ " if: steps.version.outputs.mode != 'none'",
704
709
  ' uses: actions/upload-artifact@v7.0.1',
705
710
  ' with:',
706
711
  ` name: publish-release-outputs-${githubExpression('github.run_id')}`,
@@ -714,7 +719,7 @@ function renderLinuxReleaseCandidateSteps(
714
719
  '',
715
720
  ` # Step ${stepNumber++}`,
716
721
  ' - name: 📤 Upload supplemental Linux outputs',
717
- ` if: ${githubExpression("steps.version.outputs.mode != 'none'")}`,
722
+ " if: steps.version.outputs.mode != 'none'",
718
723
  ' uses: actions/upload-artifact@v7.0.1',
719
724
  ' with:',
720
725
  ` name: publish-linux-outputs-${githubExpression('github.run_id')}`,
@@ -730,7 +735,8 @@ function renderLinuxReleaseCandidateSteps(
730
735
  '',
731
736
  ` # Step ${stepNumber}`,
732
737
  ' - name: 🧹 Cleanup and cache Nix/devenv',
733
- ` if: ${githubExpression('always()')}`,
738
+ ' # success() is default; always() still saves GH Nix cache after a red job.',
739
+ ' if: always()',
734
740
  ' uses: ./.github/actions/save-nix-devenv',
735
741
  ' with:',
736
742
  ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
@@ -779,6 +785,11 @@ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): st
779
785
  ` "${githubExpression('runner.temp')}/macos-platform-outputs"`,
780
786
  '',
781
787
  ` # Step ${stepNumber++}`,
788
+ ' - name: 🧪 Unit test macOS and iOS packages',
789
+ ' run:',
790
+ ` smoo github-ci nx-run-many --targets test --projects-with-targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}"`,
791
+ '',
792
+ ` # Step ${stepNumber++}`,
782
793
  ' - name: 📤 Upload macOS platform outputs',
783
794
  ' uses: actions/upload-artifact@v7.0.1',
784
795
  ' with:',
@@ -792,7 +803,8 @@ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): st
792
803
  '',
793
804
  ` # Step ${stepNumber}`,
794
805
  ' - name: 🧹 Cleanup and cache Nix/devenv',
795
- ` if: ${githubExpression('always()')}`,
806
+ ' # success() is default; always() still saves GH Nix cache after a red job.',
807
+ ' if: always()',
796
808
  ' uses: ./.github/actions/save-nix-devenv',
797
809
  ' with:',
798
810
  ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
@@ -876,7 +888,7 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
876
888
  '',
877
889
  ` # Step ${stepNumber++}`,
878
890
  ' - name: 📦 Apply verified Linux outputs',
879
- ` if: ${githubExpression(`${mode} != 'none'`)}`,
891
+ ` if: ${mode} != 'none'`,
880
892
  ' run:',
881
893
  ` smoo github-ci apply-outputs --source-sha "${githubExpression(
882
894
  'needs.linux-release-candidate.outputs.release-sha',
@@ -896,7 +908,7 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
896
908
  '',
897
909
  ` # Step ${stepNumber++}`,
898
910
  ' - name: 🍎 Apply verified macOS outputs',
899
- ` if: ${githubExpression(`${mode} != 'none'`)}`,
911
+ ` if: ${mode} != 'none'`,
900
912
  ' run:',
901
913
  ` smoo github-ci apply-outputs --source-sha "${githubExpression('github.sha')}"`,
902
914
  ` "${githubExpression('runner.temp')}/publish-artifacts/publish-macos-outputs-${githubExpression(
@@ -905,7 +917,7 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
905
917
  '',
906
918
  ` # Step ${stepNumber++}`,
907
919
  ` - name: ✅ Validate restored release (${githubExpression(mode)})`,
908
- ` if: ${githubExpression(`${mode} != 'none'`)}`,
920
+ ` if: ${mode} != 'none'`,
909
921
  ' run: smoo monorepo validate',
910
922
  '',
911
923
  ' # --- Release ------------------------------------------------------------',
@@ -937,7 +949,8 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
937
949
  '',
938
950
  ` # Step ${stepNumber}`,
939
951
  ' - name: 🧹 Cleanup and cache Nix/devenv',
940
- ` if: ${githubExpression('always()')}`,
952
+ ' # success() is default; always() still saves GH Nix cache after a red job.',
953
+ ' if: always()',
941
954
  ' uses: ./.github/actions/save-nix-devenv',
942
955
  ' with:',
943
956
  ` nix-cache-hit: ${githubExpression('steps.setup.outputs.nix-cache-hit')}`,
@@ -3,16 +3,16 @@ import type { PackageJson } from '../lib/json.js';
3
3
  import { runtimeTypesRangeForPublishedVersions, validateRuntimePins } from './runtime.js';
4
4
 
5
5
  describe('runtimeTypesRangeForPublishedVersions', () => {
6
- test('uses the newest published minor within the installed Node major, never the ~major.0.0 floor', () => {
6
+ test('anchors a caret range at the newest published version in the installed Node major', () => {
7
7
  expect(
8
8
  runtimeTypesRangeForPublishedVersions('@types/node', '24.12.0', 'major', ['24.0.0', '24.12.4', '25.9.1']),
9
- ).toBe('~24.12.4');
9
+ ).toBe('^24.12.4');
10
10
  });
11
11
 
12
- test('falls back to latest published @types/node when the Node major is unpublished', () => {
13
- expect(
12
+ test('rejects fallback types from a different Node major', () => {
13
+ expect(() =>
14
14
  runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['24.12.4', '25.9.0', '25.9.1']),
15
- ).toBe('~25.9.1');
15
+ ).toThrow('runtime major 26');
16
16
  });
17
17
 
18
18
  test('uses the installed Bun version when @types/bun has published it', () => {
@@ -23,10 +23,10 @@ describe('runtimeTypesRangeForPublishedVersions', () => {
23
23
  expect(runtimeTypesRangeForPublishedVersions('@types/bun', '1.3.15', 'exact', ['1.3.13', '1.3.14'])).toBe('1.3.14');
24
24
  });
25
25
 
26
- test('ignores non-stable version strings when choosing the fallback', () => {
27
- expect(runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['25.9.1', '26.0.0-beta.1'])).toBe(
28
- '~25.9.1',
29
- );
26
+ test('ignores non-stable version strings when choosing the same-major anchor', () => {
27
+ expect(
28
+ runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['25.9.1', '26.0.2', '26.1.0-beta.1']),
29
+ ).toBe('^26.0.2');
30
30
  });
31
31
  });
32
32
 
@@ -35,23 +35,29 @@ describe('validateRuntimePins', () => {
35
35
  const aligned = () => ({
36
36
  engines: { node: '>=24.0.0' },
37
37
  packageManager: 'bun@1.3.14',
38
- devDependencies: { '@types/node': '~24.13.0' },
38
+ devDependencies: { '@types/node': '^24.13.0' },
39
39
  });
40
40
 
41
- test('passes when every pin agrees with the PATH runtimes', () => {
42
- expect(validateRuntimePins(aligned(), runtime)).toBe(0);
41
+ test('accepts caret ranges anchored at any patch or minor in the PATH Node major', () => {
42
+ for (const version of ['^24.0.3', '^24.13.0']) {
43
+ const pkg = aligned();
44
+ pkg.devDependencies['@types/node'] = version;
45
+ expect(validateRuntimePins(pkg, runtime)).toBe(0);
46
+ }
43
47
  });
44
48
 
45
49
  test('fails when @types/node tracks a different major than the PATH node', () => {
46
50
  const pkg = aligned();
47
- pkg.devDependencies['@types/node'] = '~26.1.1';
51
+ pkg.devDependencies['@types/node'] = '^26.1.1';
48
52
  expect(validateRuntimePins(pkg, runtime)).toBe(1);
49
53
  });
50
54
 
51
- test('fails the ~major.0.0 floor pin even when the major matches', () => {
52
- const pkg = aligned();
53
- pkg.devDependencies['@types/node'] = '~24.0.0';
54
- expect(validateRuntimePins(pkg, runtime)).toBe(1);
55
+ test('rejects exact and tilde @types/node pins even when the major matches', () => {
56
+ for (const version of ['24.13.0', '~24.13.0']) {
57
+ const pkg = aligned();
58
+ pkg.devDependencies['@types/node'] = version;
59
+ expect(validateRuntimePins(pkg, runtime)).toBe(1);
60
+ }
55
61
  });
56
62
 
57
63
  test('fails engines.node and packageManager drift against the PATH runtimes', () => {
@@ -66,8 +66,8 @@ export async function syncRootRuntimeVersions(root: string): Promise<void> {
66
66
  /**
67
67
  * Validate that package.json runtime pins agree with the LIVE PATH runtimes,
68
68
  * never a stored template. Offline by design: structural alignment only
69
- * (majors, exact bun pin, no `~major.0.0` floor) — the registry is not
70
- * consulted. Repair: `smoo monorepo init --runtime-only` inside the devenv shell.
69
+ * (Node types float within the runtime major; Bun types stay exact) — the registry
70
+ * is not consulted. Repair: `smoo monorepo init --runtime-only` inside the devenv shell.
71
71
  */
72
72
  export async function validateRootRuntimeVersions(root: string): Promise<number> {
73
73
  const packageJson = readJsonObject(join(root, 'package.json'));
@@ -100,15 +100,10 @@ export function validateRuntimePins(packageJson: PackageJson, runtime: RuntimeVe
100
100
  failures++;
101
101
  }
102
102
  const typesNode = packageJson.devDependencies?.['@types/node'] ?? null;
103
- const typesNodeParts = typesNode ? /^~(\d+)\.(\d+)\.(\d+)$/.exec(typesNode) : null;
103
+ const typesNodeParts = typesNode ? /^\^(\d+)\.(\d+)\.(\d+)$/.exec(typesNode) : null;
104
104
  if (!typesNodeParts || typesNodeParts[1] !== nodeMajor) {
105
105
  console.error(
106
- `package.json @types/node is ${typesNode ?? 'missing'} but the PATH node is v${runtime.node} — types track the runtime major (~${nodeMajor}.x.y, newest published minor); ${repair}`,
107
- );
108
- failures++;
109
- } else if (typesNodeParts[2] === '0' && typesNodeParts[3] === '0') {
110
- console.error(
111
- `package.json @types/node is pinned to the ~${nodeMajor}.0.0 floor — tilde locks the first patch line and strands the repo on early broken releases; ${repair} to repin to the newest published ${nodeMajor}.x`,
106
+ `package.json @types/node is ${typesNode ?? 'missing'} but the PATH node is v${runtime.node} — expected a caret range within the runtime major (^${nodeMajor}.x.y); ${repair}`,
112
107
  );
113
108
  failures++;
114
109
  }
@@ -144,15 +139,15 @@ export function runtimeTypesRangeForPublishedVersions(
144
139
 
145
140
  if (pinMode === 'major') {
146
141
  const runtimeMajor = parsedRuntimeVersion[0].toString();
147
- // Newest published types WITHIN the runtime major never the `~major.0.0`
148
- // floor: tilde locks the patch line, so the floor can strand consumers on a
149
- // broken early release (e.g. @types/node 24.0.x's URLPattern/DOM TS2403
150
- // conflict, fixed in 24.13) with no path to the repaired minors.
142
+ // Anchor the range at the newest published types in the runtime major. The caret
143
+ // admits later patches and minors without crossing into a different Node major.
151
144
  const latestInMajor = latestVersion(versions.filter((version) => versionMajor(version) === runtimeMajor));
152
- if (latestInMajor) {
153
- return `~${latestInMajor}`;
145
+ if (!latestInMajor) {
146
+ throw new Error(`Unable to find published ${packageName} versions for runtime major ${runtimeMajor}`);
154
147
  }
155
- } else if (versions.includes(runtimeVersion)) {
148
+ return `^${latestInMajor}`;
149
+ }
150
+ if (versions.includes(runtimeVersion)) {
156
151
  return runtimeVersion;
157
152
  }
158
153
 
@@ -162,7 +157,7 @@ export function runtimeTypesRangeForPublishedVersions(
162
157
  }
163
158
 
164
159
  console.warn(`${packageName} has no published types for runtime ${runtimeVersion}; using ${latest}`);
165
- return pinMode === 'major' ? `~${latest}` : latest;
160
+ return latest;
166
161
  }
167
162
 
168
163
  function latestVersion(versions: readonly string[]): string | null {
@@ -19,7 +19,7 @@ const registryVersions: Record<string, string[]> = {
19
19
  'eslint-stdout': ['1.1.1', '1.1.2'],
20
20
  nx: ['23.1.0'],
21
21
  prettier: ['3.6.1'],
22
- ttsc: ['0.19.3'],
22
+ ttsc: ['0.25.0'],
23
23
  typescript: ['6.0.3'],
24
24
  // alias package is not fetched from registry under this name when using fallback
25
25
  '@typescript/native': ['7.0.2'],
@@ -50,6 +50,9 @@ describe('tool configuration validation', () => {
50
50
  devDependencies: {
51
51
  '@smoothbricks/cli': 'workspace:*',
52
52
  },
53
+ patchedDependencies: {
54
+ 'ttsc@0.19.3': 'patches/ttsc@0.19.3.patch',
55
+ },
53
56
  });
54
57
  const devenvPath = join(root, 'tooling/direnv/devenv.nix');
55
58
  await mkdir(dirname(devenvPath), { recursive: true });
@@ -76,10 +79,10 @@ describe('tool configuration validation', () => {
76
79
  expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe('workspace:*');
77
80
  expect(rootPackage.devDependencies['eslint-stdout']).toBe('workspace:*');
78
81
  expect(rootPackage.devDependencies.nx).toBe('23.1.0');
79
- expect(rootPackage.devDependencies.ttsc).toBe('0.19.3');
82
+ expect(rootPackage.devDependencies.ttsc).toBe('0.25.0');
80
83
  expect(rootPackage.devDependencies.typescript).toBe('^6.0.3');
81
84
  expect(rootPackage.devDependencies['@typescript/native']).toBe('npm:typescript@^7.0.2');
82
- expect(rootPackage.patchedDependencies['ttsc@0.19.3']).toBe('patches/ttsc@0.19.3.patch');
85
+ expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
83
86
  expect(rootPackage.workspaces).toContain('tooling');
84
87
  expect(toolingPackage.name).toBe('@smoothbricks/tooling');
85
88
  expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
@@ -91,7 +94,7 @@ describe('tool configuration validation', () => {
91
94
  }
92
95
  });
93
96
 
94
- it('accepts newer dependency versions above the policy floor', async () => {
97
+ it('accepts newer dependencies after removing even an empty obsolete ttsc patch key', async () => {
95
98
  const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
96
99
  try {
97
100
  await writeJson(join(root, 'package.json'), {
@@ -108,12 +111,12 @@ describe('tool configuration validation', () => {
108
111
  'eslint-stdout': await currentEslintStdoutRange(),
109
112
  nx: '24.0.0',
110
113
  prettier: '^3.7.0',
111
- ttsc: '0.19.3',
114
+ ttsc: '0.25.0',
112
115
  typescript: '^6.0.0',
113
116
  '@typescript/native': 'npm:typescript@^7.0.2',
114
117
  },
115
118
  patchedDependencies: {
116
- 'ttsc@0.19.3': 'patches/ttsc@0.19.3.patch',
119
+ 'ttsc@0.19.3': '',
117
120
  },
118
121
  });
119
122
  await writeJson(join(root, 'tooling/package.json'), {
@@ -143,7 +146,11 @@ describe('tool configuration validation', () => {
143
146
  `,
144
147
  );
145
148
 
149
+ expect(await validateToolConfig(root)).toBe(1);
150
+ await applyToolConfigDefaults(root);
146
151
  expect(await validateToolConfig(root)).toBe(0);
152
+ const rootPackage = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
153
+ expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
147
154
  } finally {
148
155
  await rm(root, { recursive: true, force: true });
149
156
  }
@@ -210,7 +217,7 @@ describe('tool configuration validation', () => {
210
217
  eslint: '^10.0.0',
211
218
  nx: '24.0.0',
212
219
  prettier: '^3.7.0',
213
- ttsc: '0.19.3',
220
+ ttsc: '0.25.0',
214
221
  typescript: '^6.0.0',
215
222
  },
216
223
  });
@@ -224,7 +231,7 @@ describe('tool configuration validation', () => {
224
231
  expect(rootPackage.devDependencies['eslint-stdout']).toBe(await currentEslintStdoutRange());
225
232
  expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe(await currentNxPluginRange());
226
233
  expect(rootPackage.devDependencies['@typescript/native']).toBe('npm:typescript@^7.0.2');
227
- expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBe('patches/ttsc@0.19.3.patch');
234
+ expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
228
235
  expect(toolingPackage.name).toBe('@fixture/tooling');
229
236
  expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe(await currentCliRange());
230
237
  } finally {
@@ -56,8 +56,7 @@ const rootDevDependencies: RequiredDependency[] = [
56
56
  },
57
57
  { name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
58
58
  { name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', prefix: '^' },
59
- // Exact pin: Bun patchedDependencies keys are version-locked (see patches/ttsc@0.19.3.patch).
60
- { name: 'ttsc', fallbackVersion: '0.19.3' },
59
+ { name: 'ttsc', fallbackVersion: '0.25.0' },
61
60
  // Nx and typescript-eslint still load the TypeScript JS API (6.x).
62
61
  // Compilation is exclusively delegated to ttsc by the Nx plugin targets.
63
62
  { name: 'typescript', fallbackVersion: '^6.0.3', minimumVersion: '6.0.0', prefix: '^' },
@@ -75,8 +74,7 @@ const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejan
75
74
  // never against a version template here.
76
75
  const nodePackagePattern = /(^|\s)nodejs(_\d+|_latest)?(\s|#|$)/m;
77
76
 
78
- const TTSC_PATCHED_DEPENDENCY_KEY = 'ttsc@0.19.3';
79
- const TTSC_PATCH_PATH = 'patches/ttsc@0.19.3.patch';
77
+ const obsoleteTtscPatchedDependencyKey = 'ttsc@0.19.3';
80
78
 
81
79
  export async function applyToolConfigDefaults(root: string): Promise<void> {
82
80
  const context = await readToolContext(root);
@@ -89,7 +87,7 @@ export async function validateToolConfig(root: string): Promise<number> {
89
87
  const context = await readToolContext(root);
90
88
  return (
91
89
  validateRootDevDependencies(context.policy, context.rootPackage) +
92
- validateTtscPatchedDependency(context.rootPackage) +
90
+ validateObsoleteTtscPatchRemoved(context.rootPackage) +
93
91
  validateToolingPackage(root, context.policy) +
94
92
  validateToolingWorkspace(context.rootPackage) +
95
93
  validateDevenvPackages(root)
@@ -113,7 +111,7 @@ export async function applyRootDevDependencyDefaults(root: string, context: Tool
113
111
  if (delete devDependencies['@smoothbricks/cli']) {
114
112
  changed = true;
115
113
  }
116
- changed = ensureTtscPatchedDependency(pkg) || changed;
114
+ changed = removeObsoleteTtscPatch(pkg) || changed;
117
115
  if (changed) {
118
116
  writeJsonObject(join(root, 'package.json'), pkg);
119
117
  console.log('updated package.json workspace tool dependencies');
@@ -129,7 +127,6 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
129
127
  }
130
128
  let dependencyChanged = false;
131
129
  let workspaceChanged = false;
132
- let patchChanged = false;
133
130
  const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
134
131
  for (const dependency of rootDevDependencies) {
135
132
  const current = devDependencies[dependency.name];
@@ -142,8 +139,8 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
142
139
  dependencyChanged = true;
143
140
  }
144
141
  workspaceChanged = addWorkspacePattern(pkg, 'tooling');
145
- patchChanged = ensureTtscPatchedDependency(pkg);
146
- if (dependencyChanged || workspaceChanged || patchChanged) {
142
+ dependencyChanged = removeObsoleteTtscPatch(pkg) || dependencyChanged;
143
+ if (dependencyChanged || workspaceChanged) {
147
144
  writeJsonObject(join(root, 'package.json'), pkg);
148
145
  }
149
146
  console.log(
@@ -156,11 +153,6 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
156
153
  ? 'updated package.json tooling workspace'
157
154
  : 'unchanged package.json tooling workspace',
158
155
  );
159
- console.log(
160
- patchChanged
161
- ? 'updated package.json patchedDependencies for ttsc'
162
- : 'unchanged package.json patchedDependencies for ttsc',
163
- );
164
156
  }
165
157
 
166
158
  export function applyToolingPackageDefaults(root: string, policy: ToolPolicy): void {
@@ -252,22 +244,25 @@ export function validateRootDevDependencies(policy: ToolPolicy, rootPackage: Pac
252
244
  return failures;
253
245
  }
254
246
 
255
- function ensureTtscPatchedDependency(pkg: PackageJson): boolean {
256
- const patched = ensureDependencyMap(pkg, 'patchedDependencies');
257
- return setStringProperty(patched, TTSC_PATCHED_DEPENDENCY_KEY, TTSC_PATCH_PATH);
247
+ function removeObsoleteTtscPatch(pkg: PackageJson): boolean {
248
+ const patchedDependencies = pkg.patchedDependencies;
249
+ if (!patchedDependencies || !(obsoleteTtscPatchedDependencyKey in patchedDependencies)) {
250
+ return false;
251
+ }
252
+ delete patchedDependencies[obsoleteTtscPatchedDependencyKey];
253
+ if (Object.keys(patchedDependencies).length === 0) {
254
+ delete pkg.patchedDependencies;
255
+ }
256
+ return true;
258
257
  }
259
258
 
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) {
259
+ function validateObsoleteTtscPatchRemoved(rootPackage: PackageJson | null): number {
260
+ const patchedDependencies = rootPackage?.patchedDependencies;
261
+ if (!patchedDependencies || !(obsoleteTtscPatchedDependencyKey in patchedDependencies)) {
266
262
  return 0;
267
263
  }
268
264
  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)'),
265
+ `package.json patchedDependencies.${obsoleteTtscPatchedDependencyKey} must be removed; ttsc 0.25.0 emits typia declarations without the obsolete patch`,
271
266
  );
272
267
  return 1;
273
268
  }
@@ -4,12 +4,12 @@ import {
4
4
  formatProjectTargetLines,
5
5
  nxCacheDirectories,
6
6
  nxResetCommand,
7
- nxShowProjectCommand,
8
- projectNamesFromNxShowProjectsOutput,
9
7
  projectNamesWithTarget,
10
8
  projectRootFromNxProjectJson,
9
+ projectTargetsFromNxProjects,
11
10
  targetDependenciesFromNxProjectJson,
12
11
  targetNamesFromNxProjectJson,
12
+ targetNamesFromProjects,
13
13
  targetOutputsFromNxProjectJson,
14
14
  } from './index.js';
15
15
 
@@ -18,10 +18,6 @@ describe('Nx helper command construction', () => {
18
18
  expect(nxResetCommand()).toEqual({ command: 'nx', args: ['reset'] });
19
19
  });
20
20
 
21
- it('builds explicit nx project metadata invocation', () => {
22
- expect(nxShowProjectCommand('cli')).toEqual({ command: 'nx', args: ['show', 'project', 'cli', '--json'] });
23
- });
24
-
25
21
  it('selects only local Nx cache directories', () => {
26
22
  expect(nxCacheDirectories('/repo')).toEqual([
27
23
  join('/repo', '.nx/cache'),
@@ -54,14 +50,6 @@ describe('Nx helper output formatting', () => {
54
50
  ).toEqual(['cli', 'web']);
55
51
  });
56
52
 
57
- it('parses JSON project lists from nx show projects', () => {
58
- expect(projectNamesFromNxShowProjectsOutput('["web","cli"]\n')).toEqual(['cli', 'web']);
59
- });
60
-
61
- it('parses legacy newline project lists from nx show projects', () => {
62
- expect(projectNamesFromNxShowProjectsOutput('web\ncli\n')).toEqual(['cli', 'web']);
63
- });
64
-
65
53
  it('extracts sorted target names from Nx project JSON', () => {
66
54
  expect(targetNamesFromNxProjectJson({ targets: { test: {}, build: {}, lint: {} } })).toEqual([
67
55
  'build',
@@ -113,4 +101,47 @@ describe('Nx helper output formatting', () => {
113
101
  it('treats missing target metadata as an empty project', () => {
114
102
  expect(targetNamesFromNxProjectJson({ name: 'cli' })).toEqual([]);
115
103
  });
104
+
105
+ it('projects every resolved graph node without per-project CLI parsing', () => {
106
+ const projects = {
107
+ web: {
108
+ root: 'packages/web',
109
+ targets: {
110
+ test: {
111
+ executor: '@smoothbricks/nx-plugin:bounded-exec',
112
+ dependsOn: ['build'],
113
+ options: { command: 'bun test', timeoutMs: 120_000 },
114
+ },
115
+ build: { outputs: ['{projectRoot}/dist'] },
116
+ },
117
+ },
118
+ cli: { root: 'packages/cli', targets: { lint: {} } },
119
+ };
120
+
121
+ expect(targetNamesFromProjects(projects).sort()).toEqual(['build', 'lint', 'test']);
122
+ expect(projectTargetsFromNxProjects(projects)).toEqual([
123
+ {
124
+ project: 'cli',
125
+ root: 'packages/cli',
126
+ targets: ['lint'],
127
+ buildDependsOn: undefined,
128
+ targetDependencies: new Map(),
129
+ targetExecutors: new Map(),
130
+ targetOptions: new Map(),
131
+ targetOutputs: new Map(),
132
+ targetScripts: new Map(),
133
+ },
134
+ {
135
+ project: 'web',
136
+ root: 'packages/web',
137
+ targets: ['build', 'test'],
138
+ buildDependsOn: undefined,
139
+ targetDependencies: new Map([['test', ['build']]]),
140
+ targetExecutors: new Map([['test', '@smoothbricks/nx-plugin:bounded-exec']]),
141
+ targetOptions: new Map([['test', { command: 'bun test', timeoutMs: 120_000 }]]),
142
+ targetOutputs: new Map([['build', ['{projectRoot}/dist']]]),
143
+ targetScripts: new Map(),
144
+ },
145
+ ]);
146
+ });
116
147
  });