@smoothbricks/cli 0.3.3 โ†’ 0.4.0

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 (33) hide show
  1. package/dist/cli.js +14 -0
  2. package/dist/github-ci/index.d.ts +12 -0
  3. package/dist/github-ci/index.d.ts.map +1 -1
  4. package/dist/github-ci/index.js +72 -4
  5. package/dist/monorepo/ci-workflow.d.ts +26 -0
  6. package/dist/monorepo/ci-workflow.d.ts.map +1 -0
  7. package/dist/monorepo/ci-workflow.js +201 -0
  8. package/dist/monorepo/managed-files.d.ts.map +1 -1
  9. package/dist/monorepo/managed-files.js +97 -5
  10. package/dist/monorepo/package-policy.d.ts.map +1 -1
  11. package/dist/monorepo/package-policy.js +4 -2
  12. package/dist/monorepo/publish-workflow.d.ts +7 -1
  13. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  14. package/dist/monorepo/publish-workflow.js +59 -15
  15. package/dist/monorepo/runtime.d.ts +3 -0
  16. package/dist/monorepo/runtime.d.ts.map +1 -1
  17. package/dist/monorepo/runtime.js +66 -2
  18. package/managed/raw/git-format-staged.yml +4 -0
  19. package/package.json +3 -3
  20. package/src/cli.ts +35 -5
  21. package/src/github-ci/index.ts +94 -8
  22. package/src/monorepo/__tests__/ci-workflow.test.ts +34 -0
  23. package/src/monorepo/__tests__/publish-workflow.test.ts +65 -2
  24. package/src/monorepo/ci-workflow.ts +229 -0
  25. package/src/monorepo/managed-files.ts +115 -5
  26. package/src/monorepo/package-policy.test.ts +24 -9
  27. package/src/monorepo/package-policy.ts +4 -2
  28. package/src/monorepo/publish-workflow.ts +73 -16
  29. package/src/monorepo/runtime.test.ts +30 -0
  30. package/src/monorepo/runtime.ts +94 -2
  31. package/src/release/__tests__/fixture-repo.test.ts +14 -13
  32. package/src/release/__tests__/helpers/fixture-repo.ts +85 -12
  33. package/managed/templates/github/workflows/ci.yml +0 -102
@@ -0,0 +1,229 @@
1
+ export enum CiWorkflowStepKind {
2
+ Checkout = 'checkout',
3
+ SetupDevenv = 'setup-devenv',
4
+ SetNxShas = 'set-nx-shas',
5
+ RestoreNxCache = 'restore-nx-cache',
6
+ Build = 'build',
7
+ Lint = 'lint',
8
+ UnitTests = 'unit-tests',
9
+ Deploy = 'deploy',
10
+ SaveNxCache = 'save-nx-cache',
11
+ UploadTraceDbs = 'upload-trace-dbs',
12
+ SaveNixDevenv = 'save-nix-devenv',
13
+ }
14
+
15
+ export interface CiWorkflowStep {
16
+ kind: CiWorkflowStepKind;
17
+ name: string;
18
+ number: number;
19
+ }
20
+
21
+ export interface CiWorkflowDefinitionOptions {
22
+ deploy: boolean;
23
+ deployProvider?: 'cloudflare';
24
+ pushBranches: string[];
25
+ }
26
+
27
+ type CiWorkflowStepInput = Omit<CiWorkflowStep, 'number'>;
28
+
29
+ export function defineCiWorkflow(options: CiWorkflowDefinitionOptions): CiWorkflowStep[] {
30
+ const steps: CiWorkflowStepInput[] = [
31
+ { kind: CiWorkflowStepKind.Checkout, name: '๐Ÿ“ฅ Checkout' },
32
+ { kind: CiWorkflowStepKind.SetupDevenv, name: '๐Ÿงฑ Setup Nix/devenv' },
33
+ { kind: CiWorkflowStepKind.SetNxShas, name: '๐Ÿงญ Set Nx SHAs' },
34
+ { kind: CiWorkflowStepKind.RestoreNxCache, name: '๐Ÿง  Restore Nx cache' },
35
+ { kind: CiWorkflowStepKind.Build, name: '๐Ÿ”จ Build' },
36
+ { kind: CiWorkflowStepKind.Lint, name: '๐Ÿ” Lint' },
37
+ { kind: CiWorkflowStepKind.UnitTests, name: '๐Ÿงช Unit Tests' },
38
+ ];
39
+ if (options.deploy) {
40
+ steps.push({ kind: CiWorkflowStepKind.Deploy, name: '๐Ÿš€ Deploy Staging' });
41
+ }
42
+ steps.push(
43
+ { kind: CiWorkflowStepKind.SaveNxCache, name: '๐Ÿ’พ Save Nx cache' },
44
+ { kind: CiWorkflowStepKind.UploadTraceDbs, name: '๐Ÿ“Ž Upload trace DBs' },
45
+ { kind: CiWorkflowStepKind.SaveNixDevenv, name: '๐Ÿงน Cleanup and cache Nix/devenv' },
46
+ );
47
+ return steps.map((step, index) => ({ ...step, number: index + 2 }));
48
+ }
49
+
50
+ export function renderCiWorkflowYaml(options: CiWorkflowDefinitionOptions): string {
51
+ const steps = defineCiWorkflow(options);
52
+ return `${renderCiWorkflowHeader(options)}${renderCiWorkflowSteps(steps, options)}`;
53
+ }
54
+
55
+ function renderCiWorkflowHeader(options: CiWorkflowDefinitionOptions): string {
56
+ return `name: CI
57
+
58
+ on:
59
+ push:
60
+ branches:
61
+ ${renderYamlList(options.pushBranches, 6)}
62
+ pull_request:
63
+
64
+ permissions:
65
+ actions: read
66
+ contents: read
67
+ statuses: write
68
+
69
+ defaults:
70
+ run:
71
+ working-directory: tooling/direnv
72
+
73
+ jobs:
74
+ main:
75
+ name: Validate
76
+ runs-on: ubuntu-latest
77
+ timeout-minutes: 45
78
+ env:
79
+ NIX_STORE_NAR: ${githubExpression('github.workspace')}/nix-store.nar
80
+ GH_TOKEN: ${githubExpression('github.token')}
81
+ steps:
82
+ `;
83
+ }
84
+
85
+ function githubExpression(expression: string): string {
86
+ return `$${`{{ ${expression} }}`}`;
87
+ }
88
+
89
+ function renderCiWorkflowSteps(steps: CiWorkflowStep[], options: CiWorkflowDefinitionOptions): string {
90
+ const lines: string[] = [];
91
+ for (const step of steps) {
92
+ lines.push(...sectionLinesBefore(step));
93
+ lines.push(...commentLinesForStep(step));
94
+ lines.push(...yamlLinesForStep(step, options));
95
+ lines.push('');
96
+ }
97
+ return `${lines.join('\n').trimEnd()}\n`;
98
+ }
99
+
100
+ function sectionLinesBefore(step: CiWorkflowStep): string[] {
101
+ if (step.kind === CiWorkflowStepKind.SetNxShas) {
102
+ return [' # --- Nx -----------------------------------------------------------------', ''];
103
+ }
104
+ if (step.kind === CiWorkflowStepKind.SaveNixDevenv) {
105
+ return [' # --- Cleanup ------------------------------------------------------------', ''];
106
+ }
107
+ return [];
108
+ }
109
+
110
+ function commentLinesForStep(step: CiWorkflowStep): string[] {
111
+ if (step.kind === CiWorkflowStepKind.Checkout) {
112
+ return [' # Step 1: GitHub adds "Set up job" automatically', ' # Step 2'];
113
+ }
114
+ if (step.kind === CiWorkflowStepKind.SetupDevenv) {
115
+ return [
116
+ ' # Step 3. Composite action internals do not affect top-level job step',
117
+ ' # anchors; update the nx-smart --step values below if top-level steps move.',
118
+ ];
119
+ }
120
+ if (step.kind === CiWorkflowStepKind.SetNxShas) {
121
+ return [` # Step ${step.number}`, ' # Sets the base and head SHAs required for the nx affected commands'];
122
+ }
123
+ if (step.kind === CiWorkflowStepKind.SaveNxCache) {
124
+ return [
125
+ ` # Step ${step.number}`,
126
+ " # Nx's database cache needs artifact files and .nx/workspace-data DB",
127
+ ' # metadata restored together; GitHub Actions cache is only the archive',
128
+ ' # transport. Save runs only after prior required steps succeed on the default',
129
+ ' # branch, so PRs may restore shared cache but cannot publish it.',
130
+ ];
131
+ }
132
+ return [` # Step ${step.number}`];
133
+ }
134
+
135
+ function yamlLinesForStep(step: CiWorkflowStep, options: CiWorkflowDefinitionOptions): string[] {
136
+ switch (step.kind) {
137
+ case CiWorkflowStepKind.Checkout:
138
+ return [
139
+ ` - name: ${step.name}`,
140
+ ' uses: actions/checkout@v6.0.2',
141
+ ' with:',
142
+ ' filter: blob:none',
143
+ ' fetch-depth: 0',
144
+ ];
145
+ case CiWorkflowStepKind.SetupDevenv:
146
+ return [` - name: ${step.name}`, ' id: setup', ' uses: ./.github/actions/setup-devenv'];
147
+ case CiWorkflowStepKind.SetNxShas:
148
+ return [
149
+ ` - name: ${step.name}`,
150
+ ' uses: nrwl/nx-set-shas@v5.0.1',
151
+ ' with:',
152
+ ' workflow-id: ci.yml',
153
+ ];
154
+ case CiWorkflowStepKind.RestoreNxCache:
155
+ return [` - name: ${step.name}`, ' id: nx-cache', ' uses: ./.github/actions/cache-nx'];
156
+ case CiWorkflowStepKind.Build:
157
+ return nxSmartStep(step, 'build', 'Build');
158
+ case CiWorkflowStepKind.Lint:
159
+ return nxSmartStep(step, 'lint', 'Lint');
160
+ case CiWorkflowStepKind.UnitTests:
161
+ return nxSmartStep(step, 'test', 'Unit Tests');
162
+ case CiWorkflowStepKind.Deploy:
163
+ return [
164
+ ` - name: ${step.name}`,
165
+ ' if:',
166
+ " ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}',",
167
+ ' github.event.repository.default_branch) }}',
168
+ ...deployEnvLines(options),
169
+ ` run: smoo github-ci nx-deploy --configuration staging --mode affected --name "Deploy Staging" --step ${step.number}`,
170
+ ];
171
+ case CiWorkflowStepKind.SaveNxCache:
172
+ return [
173
+ ` - name: ${step.name}`,
174
+ ' if:',
175
+ " ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}',",
176
+ " github.event.repository.default_branch) && steps.nx-cache.outputs.cache-hit != 'true' }}",
177
+ ' uses: actions/cache/save@v5.0.5',
178
+ ' with:',
179
+ ' path: |',
180
+ ' .nx/cache',
181
+ ' .nx/workspace-data/*.db*',
182
+ ' key: ${{ runner.os }}-nx-db-v1-${{ github.sha }}',
183
+ ];
184
+ case CiWorkflowStepKind.UploadTraceDbs:
185
+ return [
186
+ ` - name: ${step.name}`,
187
+ ' if: ${{ always() }}',
188
+ ' uses: actions/upload-artifact@v7.0.1',
189
+ ' with:',
190
+ ' name: trace-results-${{ github.run_id }}',
191
+ ' path: packages/*/.trace-results.db',
192
+ ' if-no-files-found: ignore',
193
+ ' retention-days: 14',
194
+ ' include-hidden-files: true',
195
+ ];
196
+ case CiWorkflowStepKind.SaveNixDevenv:
197
+ return [
198
+ ` - name: ${step.name}`,
199
+ ' if: ${{ always() }}',
200
+ ' uses: ./.github/actions/save-nix-devenv',
201
+ ' with:',
202
+ ' nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}',
203
+ ' devenv-cache-hit: ${{ steps.setup.outputs.devenv-cache-hit }}',
204
+ ];
205
+ }
206
+ }
207
+
208
+ function deployEnvLines(options: CiWorkflowDefinitionOptions): string[] {
209
+ if (options.deployProvider !== 'cloudflare') {
210
+ return [];
211
+ }
212
+ return [
213
+ ' env:',
214
+ ' CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}',
215
+ ' CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',
216
+ ];
217
+ }
218
+
219
+ function nxSmartStep(step: CiWorkflowStep, target: string, name: string): string[] {
220
+ return [
221
+ ` - name: ${step.name}`,
222
+ ` run: smoo github-ci nx-smart --target ${target} --name "${name}" --step ${step.number}`,
223
+ ];
224
+ }
225
+
226
+ function renderYamlList(values: string[], spaces: number): string {
227
+ const indent = ' '.repeat(spaces);
228
+ return values.map((value) => `${indent}- ${value}`).join('\n');
229
+ }
@@ -1,7 +1,9 @@
1
+ import { execFileSync } from 'node:child_process';
1
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { dirname, join, resolve } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
6
+ import { renderCiWorkflowYaml } from './ci-workflow.js';
5
7
  import { renderPublishWorkflowYaml } from './publish-workflow.js';
6
8
 
7
9
  type ManagedKind = 'raw' | 'template' | 'generated';
@@ -21,10 +23,20 @@ export interface FileResult {
21
23
 
22
24
  interface ManagedFileContext {
23
25
  hasReleasePackages: boolean;
26
+ hasStagingDeployTargets: boolean;
27
+ hasProductionDeployTargets: boolean;
28
+ stagingDeployProvider?: 'cloudflare';
29
+ productionDeployProvider?: 'cloudflare';
30
+ ciPushBranches: string[];
24
31
  nodeModulesCacheKey: string;
25
32
  repoName: string;
26
33
  }
27
34
 
35
+ interface DeployTargetInfo {
36
+ exists: boolean;
37
+ provider?: 'cloudflare';
38
+ }
39
+
28
40
  const managedFiles: ManagedFile[] = [
29
41
  {
30
42
  kind: 'raw',
@@ -56,8 +68,8 @@ const managedFiles: ManagedFile[] = [
56
68
  target: '.git-format-staged.yml',
57
69
  },
58
70
  {
59
- kind: 'template',
60
- source: 'github/workflows/ci.yml',
71
+ kind: 'generated',
72
+ source: 'ci-workflow',
61
73
  target: '.github/workflows/ci.yml',
62
74
  },
63
75
  {
@@ -106,7 +118,7 @@ function applyManagedFile(
106
118
  mode: 'update' | 'check' | 'diff',
107
119
  context: ManagedFileContext,
108
120
  ): FileResult {
109
- if (file.releasePackagesOnly === true && !context.hasReleasePackages) {
121
+ if (file.releasePackagesOnly === true && !context.hasReleasePackages && !context.hasProductionDeployTargets) {
110
122
  return { target: file.target, action: 'skipped' };
111
123
  }
112
124
  const target = resolve(root, file.target);
@@ -138,8 +150,19 @@ function applyManagedFile(
138
150
 
139
151
  function getManagedContent(file: ManagedFile, context: ManagedFileContext): string {
140
152
  if (file.kind === 'generated') {
153
+ if (file.source === 'ci-workflow') {
154
+ return renderCiWorkflowYaml({
155
+ deploy: context.hasStagingDeployTargets,
156
+ deployProvider: context.stagingDeployProvider,
157
+ pushBranches: context.ciPushBranches,
158
+ });
159
+ }
141
160
  if (file.source === 'publish-workflow') {
142
- return renderPublishWorkflowYaml({ repoName: context.repoName });
161
+ return renderPublishWorkflowYaml({
162
+ deploy: context.hasProductionDeployTargets,
163
+ deployProvider: context.productionDeployProvider,
164
+ repoName: context.repoName,
165
+ });
143
166
  }
144
167
  throw new Error(`Unknown generated managed file source ${file.source}`);
145
168
  }
@@ -155,18 +178,105 @@ function getManagedContent(file: ManagedFile, context: ManagedFileContext): stri
155
178
  function getManagedFileContext(root: string): ManagedFileContext {
156
179
  const packageJson = readPackageJson(join(root, 'package.json'));
157
180
  const repoName = packageJson?.name ?? 'monorepo';
181
+ const ciPushBranches = getCiPushBranches(packageJson?.json);
182
+ const stagingDeploy = getDeployTargetInfo(root, 'staging');
183
+ const productionDeploy = getDeployTargetInfo(root, 'production');
158
184
  const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
159
185
  ? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
160
186
  : `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
161
- return { hasReleasePackages: listReleasePackages(root, packageJson).length > 0, nodeModulesCacheKey, repoName };
187
+ return {
188
+ hasReleasePackages: listReleasePackages(root, packageJson).length > 0,
189
+ hasStagingDeployTargets: stagingDeploy.exists,
190
+ hasProductionDeployTargets: productionDeploy.exists,
191
+ stagingDeployProvider: stagingDeploy.provider,
192
+ productionDeployProvider: productionDeploy.provider,
193
+ ciPushBranches,
194
+ nodeModulesCacheKey,
195
+ repoName,
196
+ };
162
197
  }
163
198
 
164
199
  function renderTemplate(context: ManagedFileContext, template: string): string {
165
200
  return template
166
201
  .replaceAll('{{REPO_NAME}}', context.repoName)
202
+ .replaceAll('__SMOO_CI_PUSH_BRANCHES__', renderYamlFlowList(context.ciPushBranches))
167
203
  .replaceAll('{{NODE_MODULES_CACHE_KEY}}', context.nodeModulesCacheKey);
168
204
  }
169
205
 
206
+ function getDeployTargetInfo(root: string, configuration: string): DeployTargetInfo {
207
+ const output = execFileSync('nx', ['show', 'projects', '--withTarget', 'deploy', '--json'], {
208
+ cwd: root,
209
+ encoding: 'utf8',
210
+ stdio: ['ignore', 'pipe', 'inherit'],
211
+ });
212
+ const projects: unknown = JSON.parse(output);
213
+ if (!Array.isArray(projects) || !projects.every((project) => typeof project === 'string')) {
214
+ return { exists: false };
215
+ }
216
+ let exists = false;
217
+ let provider: DeployTargetInfo['provider'];
218
+ for (const project of projects) {
219
+ const target = nxDeployTarget(root, project, configuration);
220
+ if (!target.exists) {
221
+ continue;
222
+ }
223
+ exists = true;
224
+ provider ??= target.provider;
225
+ }
226
+ return { exists, provider };
227
+ }
228
+
229
+ function nxDeployTarget(root: string, project: string, configuration: string): DeployTargetInfo {
230
+ const output = execFileSync('nx', ['show', 'project', project, '--json'], {
231
+ cwd: root,
232
+ encoding: 'utf8',
233
+ stdio: ['ignore', 'pipe', 'inherit'],
234
+ });
235
+ const parsed: unknown = JSON.parse(output);
236
+ const projectJson = recordValue(parsed);
237
+ const targets = recordValue(projectJson?.targets);
238
+ const deploy = recordValue(targets?.deploy);
239
+ const configurations = recordValue(deploy?.configurations);
240
+ const config = recordValue(configurations?.[configuration]);
241
+ if (!config) {
242
+ return { exists: false };
243
+ }
244
+ const command = deployCommand(deploy, config);
245
+ return { exists: true, provider: command.includes('wrangler ') ? 'cloudflare' : undefined };
246
+ }
247
+
248
+ function deployCommand(deploy: Record<string, unknown> | undefined, config: Record<string, unknown>): string {
249
+ const command = stringValue(config.command) ?? stringValue(recordValue(config.options)?.command);
250
+ return command ?? stringValue(recordValue(deploy?.options)?.command) ?? '';
251
+ }
252
+
253
+ function stringValue(value: unknown): string | undefined {
254
+ return typeof value === 'string' ? value : undefined;
255
+ }
256
+
257
+ function getCiPushBranches(packageJson: unknown): string[] {
258
+ const configured = readCiPushBranches(packageJson);
259
+ return configured.length > 0 ? configured : ['main'];
260
+ }
261
+
262
+ function readCiPushBranches(packageJson: unknown): string[] {
263
+ const rootPackage = recordValue(packageJson);
264
+ const smoo = recordValue(rootPackage?.smoo);
265
+ const github = recordValue(smoo?.github);
266
+ const branches = Array.isArray(github?.pushBranches) ? github.pushBranches : [];
267
+ return branches.filter((branch): branch is string => typeof branch === 'string' && branch.length > 0);
268
+ }
269
+
270
+ function recordValue(value: unknown): Record<string, unknown> | undefined {
271
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
272
+ ? (value as Record<string, unknown>)
273
+ : undefined;
274
+ }
275
+
276
+ function renderYamlFlowList(values: string[]): string {
277
+ return JSON.stringify(values);
278
+ }
279
+
170
280
  function writeManagedFile(path: string, content: string, executable: boolean): void {
171
281
  mkdirSync(dirname(path), { recursive: true });
172
282
  writeFileSync(path, content, { mode: executable ? 0o755 : 0o644 });
@@ -5,8 +5,12 @@ import { dirname, join } from 'node:path';
5
5
  import {
6
6
  BOUNDED_TEST_EXECUTOR,
7
7
  BOUNDED_TEST_KILL_AFTER_MS,
8
+ BOUNDED_TEST_PER_TEST_TIMEOUT_MS,
8
9
  BOUNDED_TEST_TIMEOUT_MS,
9
10
  } from '@smoothbricks/nx-plugin/bounded-test-policy';
11
+
12
+ const TIMEOUT_FLAG = `--timeout=${BOUNDED_TEST_PER_TEST_TIMEOUT_MS}`;
13
+
10
14
  import {
11
15
  applyFixableMonorepoDefaults,
12
16
  applyNxPluginDefaults,
@@ -47,14 +51,17 @@ describe('root smoo monorepo policy', () => {
47
51
  );
48
52
  await writeJson(join(root, 'nx.json'), validNxJson());
49
53
 
50
- expect(validateRootPackagePolicy(root)).toBe(5);
51
- expect(validateNxReleaseConfig(root)).toBe(6);
54
+ expect(validateRootPackagePolicy(root)).toBe(7);
55
+ expect(validateNxReleaseConfig(root)).toBe(8);
52
56
 
53
57
  applyFixableMonorepoDefaults(root);
54
58
 
55
59
  const rootPackage = await readJson(join(root, 'package.json'));
56
60
  const nxJson = await readJson(join(root, 'nx.json'));
57
61
  expect(rootPackage.scripts).toEqual({
62
+ clean: 'nx run-many -t clean; nx reset',
63
+ 'clean:node_modules':
64
+ 'rm -rf node_modules && find e* t* p* -type d -name node_modules -print0 | xargs -0 rm -rvf',
58
65
  'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
59
66
  'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
60
67
  lint: 'nx run-many -t lint',
@@ -62,7 +69,7 @@ describe('root smoo monorepo policy', () => {
62
69
  });
63
70
  expect(rootPackage).toMatchObject({ nx: { includedScripts: [] } });
64
71
  expect(nxJson.namedInputs).toEqual(validNamedInputs());
65
- expect(nxJson.targetDefaults).toEqual({ build: { cache: true, outputs: ['{projectRoot}/dist'] } });
72
+ expect(nxJson.targetDefaults).toEqual(validTargetDefaults());
66
73
  expect(nxJson.plugins).toEqual([
67
74
  {
68
75
  plugin: '@nx/js/typescript',
@@ -148,12 +155,12 @@ describe('root smoo monorepo policy', () => {
148
155
  },
149
156
  });
150
157
 
151
- expect(validateNxReleaseConfig(root)).toBe(4);
158
+ expect(validateNxReleaseConfig(root)).toBe(6);
152
159
 
153
160
  applyFixableMonorepoDefaults(root);
154
161
 
155
162
  const nxJson = await readJson(join(root, 'nx.json'));
156
- expect(nxJson.targetDefaults).toEqual({ build: { cache: true, outputs: ['{projectRoot}/dist'] } });
163
+ expect(nxJson.targetDefaults).toEqual(validTargetDefaults());
157
164
  expect(validateNxReleaseConfig(root)).toBe(0);
158
165
  } finally {
159
166
  await rm(root, { recursive: true, force: true });
@@ -595,7 +602,7 @@ describe('workspace package script policy', () => {
595
602
  test: {
596
603
  executor: BOUNDED_TEST_EXECUTOR,
597
604
  options: {
598
- command: 'bun test',
605
+ command: `bun test ${TIMEOUT_FLAG}`,
599
606
  cwd: '{projectRoot}',
600
607
  timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
601
608
  killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
@@ -825,7 +832,7 @@ describe('workspace package script policy', () => {
825
832
  test: {
826
833
  executor: BOUNDED_TEST_EXECUTOR,
827
834
  options: {
828
- command: 'bun test --pass-with-no-tests',
835
+ command: `bun test ${TIMEOUT_FLAG} --pass-with-no-tests`,
829
836
  cwd: '{projectRoot}',
830
837
  timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
831
838
  killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
@@ -970,7 +977,7 @@ describe('workspace package script policy', () => {
970
977
  test: {
971
978
  executor: BOUNDED_TEST_EXECUTOR,
972
979
  options: {
973
- command: 'bun test --pass-with-no-tests',
980
+ command: `bun test ${TIMEOUT_FLAG} --pass-with-no-tests`,
974
981
  cwd: '{projectRoot}',
975
982
  timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
976
983
  killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
@@ -1076,6 +1083,7 @@ function validNxJson(): Record<string, unknown> {
1076
1083
  },
1077
1084
  releaseTag: { pattern: '{projectName}@{version}' },
1078
1085
  changelog: {
1086
+ automaticFromRef: true,
1079
1087
  workspaceChangelog: false,
1080
1088
  projectChangelogs: {
1081
1089
  createRelease: false,
@@ -1091,7 +1099,7 @@ function validConfiguredNxJson(): Record<string, unknown> {
1091
1099
  return {
1092
1100
  ...validNxJson(),
1093
1101
  namedInputs: validNamedInputs(),
1094
- targetDefaults: { build: { cache: true, outputs: ['{projectRoot}/dist'] } },
1102
+ targetDefaults: validTargetDefaults(),
1095
1103
  plugins: [
1096
1104
  {
1097
1105
  plugin: '@nx/js/typescript',
@@ -1110,6 +1118,13 @@ function validConfiguredNxJson(): Record<string, unknown> {
1110
1118
  };
1111
1119
  }
1112
1120
 
1121
+ function validTargetDefaults(): Record<string, unknown> {
1122
+ return {
1123
+ build: { cache: true, outputs: ['{projectRoot}/dist'] },
1124
+ clean: { executor: '@smoothbricks/nx-plugin:clean-outputs', cache: false },
1125
+ };
1126
+ }
1127
+
1113
1128
  function validNamedInputs(): Record<string, unknown> {
1114
1129
  return {
1115
1130
  default: ['{projectRoot}/**/*', 'sharedGlobals'],
@@ -48,10 +48,12 @@ export { SMOO_NX_RELEASE_TAG_PATTERN, SMOO_NX_VERSION_ACTIONS };
48
48
 
49
49
  const extraCommitScopes = ['release'];
50
50
  const rootScriptPolicy: Record<string, string> = {
51
+ clean: 'nx run-many -t clean; nx reset',
52
+ 'clean:node_modules': 'rm -rf node_modules && find e* t* p* -type d -name node_modules -print0 | xargs -0 rm -rvf',
53
+ 'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
54
+ 'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
51
55
  lint: 'nx run-many -t lint',
52
56
  'lint:fix': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --unstaged',
53
- 'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
54
- 'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
55
57
  };
56
58
 
57
59
  export function applyFixableMonorepoDefaults(root: string): void {