@smoothbricks/cli 0.1.1 → 0.3.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.
- package/README.md +161 -32
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +110 -10
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/generate/index.d.ts +20 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +47 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- package/dist/lib/run.d.ts +6 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +32 -0
- package/dist/lib/workspace.d.ts +25 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +79 -17
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +19 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +104 -16
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +9 -0
- package/dist/monorepo/managed-files.js +1 -1
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +13 -4
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +208 -113
- package/dist/monorepo/packed-package.d.ts +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +68 -36
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +238 -19
- package/dist/monorepo/publish-workflow.d.ts +0 -4
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +7 -15
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +25 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +112 -0
- package/dist/release/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +4 -1
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/core.js +76 -9
- package/dist/release/github-release.d.ts +1 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +3 -3
- package/dist/release/index.d.ts +32 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +488 -64
- package/dist/release/npm-auth.d.ts +0 -1
- package/dist/release/npm-auth.d.ts.map +1 -1
- package/dist/release/npm-auth.js +4 -16
- package/dist/release/orchestration.d.ts +11 -2
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +41 -3
- package/dist/release/publish-plan.js +1 -1
- package/managed/raw/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +4 -2
- package/package.json +15 -8
- package/src/cli.ts +153 -22
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/generate/index.ts +92 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +44 -0
- package/src/lib/workspace.ts +112 -17
- package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +142 -16
- package/src/monorepo/lockfile.ts +9 -0
- package/src/monorepo/managed-files.ts +1 -1
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +1130 -0
- package/src/monorepo/package-policy.ts +237 -114
- package/src/monorepo/packed-package.ts +89 -37
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +311 -20
- package/src/monorepo/publish-workflow.ts +10 -19
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +73 -0
- package/src/nx/index.ts +139 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +262 -0
- package/src/release/__tests__/core-properties.test.ts +6 -6
- package/src/release/__tests__/core-scenarios.test.ts +27 -20
- package/src/release/__tests__/core.test.ts +52 -5
- package/src/release/__tests__/fixture-repo.test.ts +90 -31
- package/src/release/__tests__/github-release.test.ts +22 -24
- package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
- package/src/release/__tests__/npm-auth.test.ts +12 -21
- package/src/release/__tests__/orchestration.test.ts +142 -12
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +10 -10
- package/src/release/__tests__/trust-publisher.test.ts +165 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +96 -10
- package/src/release/github-release.ts +3 -3
- package/src/release/index.ts +658 -74
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +60 -5
- package/src/release/publish-plan.ts +1 -1
- package/dist/nx-version-actions.cjs +0 -25
- package/dist/nx-version-actions.d.cts +0 -6
- package/dist/nx-version-actions.d.cts.map +0 -1
- package/managed/raw/.git-format-staged.yml +0 -47
- package/src/nx-version-actions.cts +0 -36
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, spyOn } from 'bun:test';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
BOUNDED_TEST_EXECUTOR,
|
|
7
|
+
BOUNDED_TEST_KILL_AFTER_MS,
|
|
8
|
+
BOUNDED_TEST_TIMEOUT_MS,
|
|
9
|
+
} from '@smoothbricks/nx-plugin/bounded-test-policy';
|
|
10
|
+
import {
|
|
11
|
+
applyFixableMonorepoDefaults,
|
|
12
|
+
applyNxPluginDefaults,
|
|
13
|
+
applyNxProjectNameDefaults,
|
|
14
|
+
applyWorkspaceDependencyDefaults,
|
|
15
|
+
listValidCommitScopes,
|
|
16
|
+
validateNxProjectNames,
|
|
17
|
+
validateNxReleaseConfig,
|
|
18
|
+
validateRootPackagePolicy,
|
|
19
|
+
validateWorkspaceDependencies,
|
|
20
|
+
} from './package-policy.js';
|
|
21
|
+
|
|
22
|
+
const buildOutputDependencies = [
|
|
23
|
+
'^build',
|
|
24
|
+
'*-js',
|
|
25
|
+
'*-web',
|
|
26
|
+
'*-html',
|
|
27
|
+
'*-css',
|
|
28
|
+
'*-ios',
|
|
29
|
+
'*-android',
|
|
30
|
+
'*-native',
|
|
31
|
+
'*-napi',
|
|
32
|
+
'*-bun',
|
|
33
|
+
'*-wasm',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe('root smoo monorepo policy', () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
console.error = originalConsoleError;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('fixes root scripts and Nx plugin defaults', async () => {
|
|
42
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
43
|
+
try {
|
|
44
|
+
await writeJson(
|
|
45
|
+
join(root, 'package.json'),
|
|
46
|
+
validRootPackage({ scripts: { lint: 'nx affected -t lint' }, nx: {} }),
|
|
47
|
+
);
|
|
48
|
+
await writeJson(join(root, 'nx.json'), validNxJson());
|
|
49
|
+
|
|
50
|
+
expect(validateRootPackagePolicy(root)).toBe(5);
|
|
51
|
+
expect(validateNxReleaseConfig(root)).toBe(6);
|
|
52
|
+
|
|
53
|
+
applyFixableMonorepoDefaults(root);
|
|
54
|
+
|
|
55
|
+
const rootPackage = await readJson(join(root, 'package.json'));
|
|
56
|
+
const nxJson = await readJson(join(root, 'nx.json'));
|
|
57
|
+
expect(rootPackage.scripts).toEqual({
|
|
58
|
+
'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
|
|
59
|
+
'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
|
|
60
|
+
lint: 'nx run-many -t lint',
|
|
61
|
+
'lint:fix': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --unstaged',
|
|
62
|
+
});
|
|
63
|
+
expect(rootPackage).toMatchObject({ nx: { includedScripts: [] } });
|
|
64
|
+
expect(nxJson.namedInputs).toEqual(validNamedInputs());
|
|
65
|
+
expect(nxJson.targetDefaults).toEqual({ build: { cache: true, outputs: ['{projectRoot}/dist'] } });
|
|
66
|
+
expect(nxJson.plugins).toEqual([
|
|
67
|
+
{
|
|
68
|
+
plugin: '@nx/js/typescript',
|
|
69
|
+
options: {
|
|
70
|
+
typecheck: { targetName: 'typecheck' },
|
|
71
|
+
build: {
|
|
72
|
+
targetName: 'tsc-js',
|
|
73
|
+
configName: 'tsconfig.lib.json',
|
|
74
|
+
buildDepsName: 'build-deps',
|
|
75
|
+
watchDepsName: 'watch-deps',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
'@smoothbricks/nx-plugin',
|
|
80
|
+
]);
|
|
81
|
+
expect(validateRootPackagePolicy(root)).toBe(0);
|
|
82
|
+
expect(validateNxReleaseConfig(root)).toBe(0);
|
|
83
|
+
} finally {
|
|
84
|
+
await rm(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rejects broad production named inputs and fixes them precisely', async () => {
|
|
89
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
90
|
+
try {
|
|
91
|
+
await writeJson(join(root, 'package.json'), validRootPackage());
|
|
92
|
+
await writeJson(join(root, 'nx.json'), {
|
|
93
|
+
...validConfiguredNxJson(),
|
|
94
|
+
namedInputs: {
|
|
95
|
+
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
96
|
+
production: ['default', '{projectRoot}/**/*'],
|
|
97
|
+
sharedGlobals: ['{workspaceRoot}/.github/workflows/ci.yml'],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const errors = captureConsoleErrors();
|
|
101
|
+
|
|
102
|
+
expect(validateNxReleaseConfig(root)).toBe(1);
|
|
103
|
+
expect(errors.join('\n')).toContain('namedInputs.production must enumerate precise production inputs');
|
|
104
|
+
|
|
105
|
+
applyNxPluginDefaults(root);
|
|
106
|
+
|
|
107
|
+
const nxJson = await readJson(join(root, 'nx.json'));
|
|
108
|
+
expect(nxJson.namedInputs).toEqual(validNamedInputs());
|
|
109
|
+
expect(validateNxReleaseConfig(root)).toBe(0);
|
|
110
|
+
} finally {
|
|
111
|
+
await rm(root, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('accepts custom precise production named inputs for non-TypeScript tools', async () => {
|
|
116
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
117
|
+
try {
|
|
118
|
+
await writeJson(join(root, 'package.json'), validRootPackage());
|
|
119
|
+
await writeJson(join(root, 'nx.json'), {
|
|
120
|
+
...validConfiguredNxJson(),
|
|
121
|
+
namedInputs: {
|
|
122
|
+
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
123
|
+
production: [
|
|
124
|
+
'{projectRoot}/Cargo.toml',
|
|
125
|
+
'{projectRoot}/src/**/*.rs',
|
|
126
|
+
'{projectRoot}/pyproject.toml',
|
|
127
|
+
'{projectRoot}/python/**/*.py',
|
|
128
|
+
],
|
|
129
|
+
sharedGlobals: ['{workspaceRoot}/.github/workflows/ci.yml'],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(validateNxReleaseConfig(root)).toBe(0);
|
|
134
|
+
} finally {
|
|
135
|
+
await rm(root, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('rejects all colon-style Nx target defaults', async () => {
|
|
140
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
141
|
+
try {
|
|
142
|
+
await writeJson(join(root, 'package.json'), validRootPackage());
|
|
143
|
+
await writeJson(join(root, 'nx.json'), {
|
|
144
|
+
...validConfiguredNxJson(),
|
|
145
|
+
targetDefaults: {
|
|
146
|
+
'build:wasm': {},
|
|
147
|
+
'lint:fix': {},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(validateNxReleaseConfig(root)).toBe(4);
|
|
152
|
+
|
|
153
|
+
applyFixableMonorepoDefaults(root);
|
|
154
|
+
|
|
155
|
+
const nxJson = await readJson(join(root, 'nx.json'));
|
|
156
|
+
expect(nxJson.targetDefaults).toEqual({ build: { cache: true, outputs: ['{projectRoot}/dist'] } });
|
|
157
|
+
expect(validateNxReleaseConfig(root)).toBe(0);
|
|
158
|
+
} finally {
|
|
159
|
+
await rm(root, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('explains Nx plugin conventions when nx.json is not configured', async () => {
|
|
164
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
165
|
+
try {
|
|
166
|
+
await writeJson(join(root, 'package.json'), validRootPackage());
|
|
167
|
+
await writeJson(join(root, 'nx.json'), {
|
|
168
|
+
...validConfiguredNxJson(),
|
|
169
|
+
plugins: [],
|
|
170
|
+
});
|
|
171
|
+
const errors = captureConsoleErrors();
|
|
172
|
+
|
|
173
|
+
expect(validateNxReleaseConfig(root)).toBe(2);
|
|
174
|
+
|
|
175
|
+
expect(errors.join('\n')).toContain('Official Nx owns TypeScript library inference');
|
|
176
|
+
expect(errors.join('\n')).toContain('tsconfig.lib.json produces tsc-js');
|
|
177
|
+
expect(errors.join('\n')).toContain('Smoo relies on this plugin to infer convention targets');
|
|
178
|
+
expect(errors.join('\n')).not.toContain('Fix:');
|
|
179
|
+
} finally {
|
|
180
|
+
await rm(root, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('explains why TypeScript build targetName is tsc-js', async () => {
|
|
185
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
186
|
+
try {
|
|
187
|
+
await writeJson(join(root, 'package.json'), validRootPackage());
|
|
188
|
+
await writeJson(join(root, 'nx.json'), {
|
|
189
|
+
...validConfiguredNxJson(),
|
|
190
|
+
plugins: [
|
|
191
|
+
{
|
|
192
|
+
plugin: '@nx/js/typescript',
|
|
193
|
+
options: { build: { targetName: 'build', configName: 'tsconfig.lib.json' } },
|
|
194
|
+
},
|
|
195
|
+
'@smoothbricks/nx-plugin',
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
const errors = captureConsoleErrors();
|
|
199
|
+
|
|
200
|
+
expect(validateNxReleaseConfig(root)).toBe(1);
|
|
201
|
+
|
|
202
|
+
expect(errors.join('\n')).toContain('build.targetName must be tsc-js');
|
|
203
|
+
expect(errors.join('\n')).toContain('build is reserved for aggregate targets');
|
|
204
|
+
expect(errors.join('\n')).not.toContain('Fix:');
|
|
205
|
+
} finally {
|
|
206
|
+
await rm(root, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Nx project name policy', () => {
|
|
212
|
+
it('fixes same-scope packages without touching external or unscoped packages', async () => {
|
|
213
|
+
const root = await createWorkspace({
|
|
214
|
+
rootName: '@smoothbricks/codebase',
|
|
215
|
+
packages: [
|
|
216
|
+
{ dir: 'cli', name: '@smoothbricks/cli', nx: { tags: ['npm:public'] } },
|
|
217
|
+
{ dir: 'external', name: '@external/thing' },
|
|
218
|
+
{ dir: 'tool', name: 'eslint-stdout' },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
expect(validateNxProjectNames(root)).toBe(1);
|
|
223
|
+
|
|
224
|
+
applyNxProjectNameDefaults(root);
|
|
225
|
+
|
|
226
|
+
const cli = JSON.parse(await readFile(join(root, 'packages/cli/package.json'), 'utf8'));
|
|
227
|
+
const external = JSON.parse(await readFile(join(root, 'packages/external/package.json'), 'utf8'));
|
|
228
|
+
const tool = JSON.parse(await readFile(join(root, 'packages/tool/package.json'), 'utf8'));
|
|
229
|
+
expect(cli.nx).toEqual({ tags: ['npm:public'], name: 'cli' });
|
|
230
|
+
expect(external.nx).toBeUndefined();
|
|
231
|
+
expect(tool.nx).toBeUndefined();
|
|
232
|
+
expect(validateNxProjectNames(root)).toBe(0);
|
|
233
|
+
expect(listValidCommitScopes(root)).toEqual(new Set(['cli', 'release']));
|
|
234
|
+
} finally {
|
|
235
|
+
await rm(root, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('accepts private unversioned workspace projects as commit scopes', async () => {
|
|
240
|
+
const root = await createWorkspace({
|
|
241
|
+
rootName: '@smoothbricks/codebase',
|
|
242
|
+
packages: [{ dir: 'tooling', name: '@smoothbricks/tooling', private: true, version: null }],
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
expect(validateNxProjectNames(root)).toBe(1);
|
|
246
|
+
|
|
247
|
+
applyNxProjectNameDefaults(root);
|
|
248
|
+
|
|
249
|
+
const tooling = JSON.parse(await readFile(join(root, 'packages/tooling/package.json'), 'utf8'));
|
|
250
|
+
expect(tooling.version).toBeUndefined();
|
|
251
|
+
expect(tooling.nx).toEqual({ name: 'tooling' });
|
|
252
|
+
expect(validateNxProjectNames(root)).toBe(0);
|
|
253
|
+
expect(listValidCommitScopes(root)).toEqual(new Set(['release', 'tooling']));
|
|
254
|
+
} finally {
|
|
255
|
+
await rm(root, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('workspace package script policy', () => {
|
|
261
|
+
it('rejects all colon-style package Nx targets', async () => {
|
|
262
|
+
const root = await createWorkspace({
|
|
263
|
+
rootName: '@smoothbricks/codebase',
|
|
264
|
+
packages: [
|
|
265
|
+
{
|
|
266
|
+
dir: 'native',
|
|
267
|
+
name: '@smoothbricks/native',
|
|
268
|
+
nx: { name: 'native', targets: { 'build:ts': {}, 'lint:fix': {} } },
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
expect(validateWorkspaceDependencies(root)).toBe(2);
|
|
274
|
+
|
|
275
|
+
const resolvedTargetsByProject = new Map([['native', new Set(['build', 'tsc-js', 'zig-wasm'])]]);
|
|
276
|
+
applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
|
|
277
|
+
|
|
278
|
+
const native = await readJson(join(root, 'packages/native/package.json'));
|
|
279
|
+
expect(native.nx).toEqual({ name: 'native', targets: {} });
|
|
280
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
281
|
+
} finally {
|
|
282
|
+
await rm(root, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('migrates colon build target aliases before removing colon targets', async () => {
|
|
287
|
+
const root = await createWorkspace({
|
|
288
|
+
rootName: '@smoothbricks/codebase',
|
|
289
|
+
packages: [
|
|
290
|
+
{ dir: 'lib', name: '@smoothbricks/lib', nx: { name: 'lib' } },
|
|
291
|
+
{
|
|
292
|
+
dir: 'native',
|
|
293
|
+
name: '@smoothbricks/native',
|
|
294
|
+
dependencies: { '@smoothbricks/lib': 'workspace:*' },
|
|
295
|
+
scripts: {
|
|
296
|
+
'build:custom': 'nx run native:build:custom',
|
|
297
|
+
'build:zig': 'nx run native:build:zig',
|
|
298
|
+
'build:ts': 'nx run native:build:ts',
|
|
299
|
+
},
|
|
300
|
+
nx: {
|
|
301
|
+
name: 'native',
|
|
302
|
+
targets: {
|
|
303
|
+
build: { dependsOn: ['^build', 'build:ts', 'build:zig', 'build:custom'] },
|
|
304
|
+
'build:custom': {},
|
|
305
|
+
'build:zig': {
|
|
306
|
+
executor: 'nx:run-commands',
|
|
307
|
+
options: { command: 'zig build wasm', cwd: '{projectRoot}' },
|
|
308
|
+
},
|
|
309
|
+
'build:ts': {
|
|
310
|
+
executor: 'nx:run-commands',
|
|
311
|
+
options: { command: 'tsc --build tsconfig.lib.json', cwd: '{projectRoot}' },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
try {
|
|
319
|
+
await writeJson(join(root, 'packages/native/tsconfig.lib.json'), {});
|
|
320
|
+
await writeFile(
|
|
321
|
+
join(root, 'packages/native/build.zig'),
|
|
322
|
+
'pub fn build(b: *std.Build) void { _ = b.step("wasm", "Build wasm"); }\n',
|
|
323
|
+
);
|
|
324
|
+
expect(validateWorkspaceDependencies(root)).toBe(6);
|
|
325
|
+
|
|
326
|
+
const resolvedTargetsByProject = new Map([['native', new Set(['build', 'custom', 'tsc-js', 'zig-wasm'])]]);
|
|
327
|
+
applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
|
|
328
|
+
|
|
329
|
+
const native = await readJson(join(root, 'packages/native/package.json'));
|
|
330
|
+
expect(native.scripts).toEqual({
|
|
331
|
+
'build:custom': 'nx run native:custom',
|
|
332
|
+
'build:zig': 'nx run native:zig-wasm',
|
|
333
|
+
'build:ts': 'nx run native:tsc-js',
|
|
334
|
+
});
|
|
335
|
+
expect(native.nx).toEqual({
|
|
336
|
+
name: 'native',
|
|
337
|
+
targets: {
|
|
338
|
+
build: { dependsOn: ['^build', 'tsc-js', 'zig-wasm', 'custom'] },
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
expect(validateWorkspaceDependencies(root, { resolvedTargetsByProject })).toBe(0);
|
|
342
|
+
} finally {
|
|
343
|
+
await rm(root, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('keeps noop aggregate build targets when they contain unresolved dependency wiring', async () => {
|
|
348
|
+
const root = await createWorkspace({
|
|
349
|
+
rootName: '@smoothbricks/codebase',
|
|
350
|
+
packages: [
|
|
351
|
+
{
|
|
352
|
+
dir: 'native',
|
|
353
|
+
name: '@smoothbricks/native',
|
|
354
|
+
nx: {
|
|
355
|
+
name: 'native',
|
|
356
|
+
targets: {
|
|
357
|
+
build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'zig-wasm', 'custom'] },
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
await writeJson(join(root, 'packages/native/tsconfig.lib.json'), {});
|
|
365
|
+
await writeFile(
|
|
366
|
+
join(root, 'packages/native/build.zig'),
|
|
367
|
+
'pub fn build(b: *std.Build) void { _ = b.step("wasm", "Build wasm"); }\n',
|
|
368
|
+
);
|
|
369
|
+
const resolvedTargetsByProject = new Map([
|
|
370
|
+
[
|
|
371
|
+
'native',
|
|
372
|
+
{ targets: new Set(['build', 'tsc-js', 'zig-wasm']), buildDependsOn: ['^build', 'tsc-js', 'zig-wasm'] },
|
|
373
|
+
],
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
|
|
377
|
+
|
|
378
|
+
const native = await readJson(join(root, 'packages/native/package.json'));
|
|
379
|
+
expect(native.nx).toEqual({
|
|
380
|
+
name: 'native',
|
|
381
|
+
targets: {
|
|
382
|
+
build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'zig-wasm', 'custom'] },
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
} finally {
|
|
386
|
+
await rm(root, { recursive: true, force: true });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('keeps non-noop build targets even when their dependencies are resolved', async () => {
|
|
391
|
+
const root = await createWorkspace({
|
|
392
|
+
rootName: '@smoothbricks/codebase',
|
|
393
|
+
packages: [
|
|
394
|
+
{
|
|
395
|
+
dir: 'native',
|
|
396
|
+
name: '@smoothbricks/native',
|
|
397
|
+
nx: {
|
|
398
|
+
name: 'native',
|
|
399
|
+
targets: {
|
|
400
|
+
build: {
|
|
401
|
+
executor: 'nx:run-commands',
|
|
402
|
+
options: { command: 'zig build wasm', cwd: '{projectRoot}' },
|
|
403
|
+
dependsOn: ['^build', 'zig-wasm'],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
await writeFile(
|
|
412
|
+
join(root, 'packages/native/build.zig'),
|
|
413
|
+
'pub fn build(b: *std.Build) void { _ = b.step("wasm", "Build wasm"); }\n',
|
|
414
|
+
);
|
|
415
|
+
const resolvedTargetsByProject = new Map([
|
|
416
|
+
['native', { targets: new Set(['build', 'zig-wasm']), buildDependsOn: ['^build', 'zig-wasm'] }],
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
|
|
420
|
+
|
|
421
|
+
const native = await readJson(join(root, 'packages/native/package.json'));
|
|
422
|
+
expect(native.nx).toEqual({
|
|
423
|
+
name: 'native',
|
|
424
|
+
targets: {
|
|
425
|
+
build: {
|
|
426
|
+
executor: 'nx:run-commands',
|
|
427
|
+
options: { command: 'zig build wasm', cwd: '{projectRoot}' },
|
|
428
|
+
dependsOn: ['^build', 'zig-wasm'],
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
} finally {
|
|
433
|
+
await rm(root, { recursive: true, force: true });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('removes noop aggregate build targets only when they match resolved Nx plugin output', async () => {
|
|
438
|
+
const root = await createWorkspace({
|
|
439
|
+
rootName: '@smoothbricks/codebase',
|
|
440
|
+
packages: [
|
|
441
|
+
{
|
|
442
|
+
dir: 'native',
|
|
443
|
+
name: '@smoothbricks/native',
|
|
444
|
+
nx: {
|
|
445
|
+
name: 'native',
|
|
446
|
+
targets: {
|
|
447
|
+
build: { executor: 'nx:noop', dependsOn: ['^build', 'tsc-js', 'zig-wasm'] },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
});
|
|
453
|
+
try {
|
|
454
|
+
const resolvedTargetsByProject = new Map([
|
|
455
|
+
[
|
|
456
|
+
'native',
|
|
457
|
+
{ targets: new Set(['build', 'tsc-js', 'zig-wasm']), buildDependsOn: ['^build', 'tsc-js', 'zig-wasm'] },
|
|
458
|
+
],
|
|
459
|
+
]);
|
|
460
|
+
|
|
461
|
+
applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
|
|
462
|
+
|
|
463
|
+
const native = await readJson(join(root, 'packages/native/package.json'));
|
|
464
|
+
expect(native.nx).toEqual({ name: 'native', targets: {} });
|
|
465
|
+
} finally {
|
|
466
|
+
await rm(root, { recursive: true, force: true });
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('accepts wildcard aggregate build dependencies', async () => {
|
|
471
|
+
const root = await createWorkspace({
|
|
472
|
+
rootName: '@smoothbricks/codebase',
|
|
473
|
+
packages: [
|
|
474
|
+
{
|
|
475
|
+
dir: 'native',
|
|
476
|
+
name: '@smoothbricks/native',
|
|
477
|
+
nx: {
|
|
478
|
+
name: 'native',
|
|
479
|
+
targets: {
|
|
480
|
+
build: { executor: 'nx:noop', dependsOn: buildOutputDependencies },
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
});
|
|
486
|
+
try {
|
|
487
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
488
|
+
} finally {
|
|
489
|
+
await rm(root, { recursive: true, force: true });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('removes noop aggregate build targets when wildcard dependencies match resolved Nx plugin output', async () => {
|
|
494
|
+
const root = await createWorkspace({
|
|
495
|
+
rootName: '@smoothbricks/codebase',
|
|
496
|
+
packages: [
|
|
497
|
+
{
|
|
498
|
+
dir: 'native',
|
|
499
|
+
name: '@smoothbricks/native',
|
|
500
|
+
nx: {
|
|
501
|
+
name: 'native',
|
|
502
|
+
targets: {
|
|
503
|
+
build: { executor: 'nx:noop', dependsOn: buildOutputDependencies },
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
const resolvedTargetsByProject = new Map([
|
|
511
|
+
['native', { targets: new Set(['build', 'tsc-js', 'zig-wasm']), buildDependsOn: buildOutputDependencies }],
|
|
512
|
+
]);
|
|
513
|
+
|
|
514
|
+
applyWorkspaceDependencyDefaults(root, { resolvedTargetsByProject });
|
|
515
|
+
|
|
516
|
+
const native = await readJson(join(root, 'packages/native/package.json'));
|
|
517
|
+
expect(native.nx).toEqual({ name: 'native', targets: {} });
|
|
518
|
+
} finally {
|
|
519
|
+
await rm(root, { recursive: true, force: true });
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('preserves explicit repo-local output targets with cache metadata', async () => {
|
|
524
|
+
const root = await createWorkspace({
|
|
525
|
+
rootName: '@smoothbricks/codebase',
|
|
526
|
+
packages: [
|
|
527
|
+
{
|
|
528
|
+
dir: 'bundled',
|
|
529
|
+
name: '@smoothbricks/bundled',
|
|
530
|
+
nx: {
|
|
531
|
+
name: 'bundled',
|
|
532
|
+
targets: {
|
|
533
|
+
build: { executor: 'nx:noop', dependsOn: buildOutputDependencies },
|
|
534
|
+
'tsdown-js': {
|
|
535
|
+
executor: 'nx:run-commands',
|
|
536
|
+
cache: true,
|
|
537
|
+
inputs: ['production', '^production'],
|
|
538
|
+
outputs: ['{projectRoot}/dist'],
|
|
539
|
+
options: { command: 'tsdown', cwd: '{projectRoot}' },
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
});
|
|
546
|
+
try {
|
|
547
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
548
|
+
|
|
549
|
+
applyWorkspaceDependencyDefaults(root);
|
|
550
|
+
|
|
551
|
+
const bundled = await readJson(join(root, 'packages/bundled/package.json'));
|
|
552
|
+
expect(bundled).toMatchObject({
|
|
553
|
+
nx: {
|
|
554
|
+
targets: {
|
|
555
|
+
build: { dependsOn: buildOutputDependencies },
|
|
556
|
+
'tsdown-js': {
|
|
557
|
+
executor: 'nx:run-commands',
|
|
558
|
+
cache: true,
|
|
559
|
+
inputs: ['production', '^production'],
|
|
560
|
+
outputs: ['{projectRoot}/dist'],
|
|
561
|
+
options: { command: 'tsdown', cwd: '{projectRoot}' },
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
} finally {
|
|
567
|
+
await rm(root, { recursive: true, force: true });
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('rejects build.zig without steps', async () => {
|
|
572
|
+
const root = await createWorkspace({
|
|
573
|
+
rootName: '@smoothbricks/codebase',
|
|
574
|
+
packages: [{ dir: 'native', name: '@smoothbricks/native', nx: { name: 'native' } }],
|
|
575
|
+
});
|
|
576
|
+
try {
|
|
577
|
+
await writeFile(join(root, 'packages/native/build.zig'), 'pub fn build(b: *std.Build) void { _ = b; }\n');
|
|
578
|
+
|
|
579
|
+
expect(validateWorkspaceDependencies(root)).toBe(1);
|
|
580
|
+
} finally {
|
|
581
|
+
await rm(root, { recursive: true, force: true });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('rejects old test tsconfig output for packages with test runners', async () => {
|
|
586
|
+
const root = await createWorkspace({
|
|
587
|
+
rootName: '@smoothbricks/codebase',
|
|
588
|
+
packages: [
|
|
589
|
+
{
|
|
590
|
+
dir: 'native',
|
|
591
|
+
name: '@smoothbricks/native',
|
|
592
|
+
nx: {
|
|
593
|
+
name: 'native',
|
|
594
|
+
targets: {
|
|
595
|
+
test: {
|
|
596
|
+
executor: BOUNDED_TEST_EXECUTOR,
|
|
597
|
+
options: {
|
|
598
|
+
command: 'bun test',
|
|
599
|
+
cwd: '{projectRoot}',
|
|
600
|
+
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
601
|
+
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
scripts: { test: 'nx run native:test --tui=false --outputStyle=stream' },
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
});
|
|
610
|
+
try {
|
|
611
|
+
await writeJson(join(root, 'packages/native/tsconfig.test.json'), {
|
|
612
|
+
compilerOptions: {
|
|
613
|
+
composite: true,
|
|
614
|
+
declaration: true,
|
|
615
|
+
declarationMap: true,
|
|
616
|
+
outDir: 'dist-test',
|
|
617
|
+
tsBuildInfoFile: 'dist-test/tsconfig.test.tsbuildinfo',
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
expect(validateWorkspaceDependencies(root)).toBe(6);
|
|
622
|
+
} finally {
|
|
623
|
+
await rm(root, { recursive: true, force: true });
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('creates no-emit test typecheck config for packages that use bun test', async () => {
|
|
628
|
+
const root = await createWorkspace({
|
|
629
|
+
rootName: '@smoothbricks/codebase',
|
|
630
|
+
packages: [{ dir: 'app', name: '@smoothbricks/app', scripts: { test: 'bun test --pass-with-no-tests' } }],
|
|
631
|
+
});
|
|
632
|
+
try {
|
|
633
|
+
await writeJson(join(root, 'packages/app/tsconfig.json'), {
|
|
634
|
+
files: [],
|
|
635
|
+
include: [],
|
|
636
|
+
references: [{ path: './tsconfig.lib.json' }, { path: './tsconfig.test.json' }],
|
|
637
|
+
});
|
|
638
|
+
await writeJson(join(root, 'packages/app/tsconfig.lib.json'), {
|
|
639
|
+
extends: '../../tsconfig.base.json',
|
|
640
|
+
compilerOptions: { baseUrl: '.', rootDir: 'src', types: ['node'], outDir: 'dist' },
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(validateWorkspaceDependencies(root)).toBe(2);
|
|
644
|
+
|
|
645
|
+
applyWorkspaceDependencyDefaults(root);
|
|
646
|
+
|
|
647
|
+
const testTsconfig = await readJson(join(root, 'packages/app/tsconfig.test.json'));
|
|
648
|
+
expect(testTsconfig).toEqual({
|
|
649
|
+
extends: '../../tsconfig.base.json',
|
|
650
|
+
compilerOptions: {
|
|
651
|
+
baseUrl: '.',
|
|
652
|
+
composite: false,
|
|
653
|
+
declaration: false,
|
|
654
|
+
declarationMap: false,
|
|
655
|
+
emitDeclarationOnly: false,
|
|
656
|
+
noEmit: true,
|
|
657
|
+
types: ['bun'],
|
|
658
|
+
},
|
|
659
|
+
include: [
|
|
660
|
+
'src/**/*.test.ts',
|
|
661
|
+
'src/**/*.spec.ts',
|
|
662
|
+
'src/**/__tests__/**/*.ts',
|
|
663
|
+
'src/**/__tests__/**/*.tsx',
|
|
664
|
+
'src/test-suite-tracer.ts',
|
|
665
|
+
],
|
|
666
|
+
references: [{ path: './tsconfig.lib.json' }],
|
|
667
|
+
});
|
|
668
|
+
const projectTsconfig = await readJson(join(root, 'packages/app/tsconfig.json'));
|
|
669
|
+
expect(projectTsconfig.references).toEqual([{ path: './tsconfig.lib.json' }]);
|
|
670
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
671
|
+
} finally {
|
|
672
|
+
await rm(root, { recursive: true, force: true });
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('creates no-emit test typecheck config for packages that use vitest', async () => {
|
|
677
|
+
const root = await createWorkspace({
|
|
678
|
+
rootName: '@smoothbricks/codebase',
|
|
679
|
+
packages: [{ dir: 'app', name: '@smoothbricks/app', scripts: { test: 'vitest run' } }],
|
|
680
|
+
});
|
|
681
|
+
try {
|
|
682
|
+
await writeJson(join(root, 'packages/app/tsconfig.lib.json'), {
|
|
683
|
+
extends: '../../tsconfig.base.json',
|
|
684
|
+
compilerOptions: { baseUrl: '.', rootDir: 'src', outDir: 'dist' },
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
expect(validateWorkspaceDependencies(root)).toBe(2);
|
|
688
|
+
|
|
689
|
+
applyWorkspaceDependencyDefaults(root);
|
|
690
|
+
|
|
691
|
+
const testTsconfig = await readJson(join(root, 'packages/app/tsconfig.test.json'));
|
|
692
|
+
expect(testTsconfig).toMatchObject({ compilerOptions: { noEmit: true } });
|
|
693
|
+
expect(testTsconfig).not.toMatchObject({ compilerOptions: { types: expect.anything() } });
|
|
694
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
695
|
+
} finally {
|
|
696
|
+
await rm(root, { recursive: true, force: true });
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('does not require tsconfig.test.json for other test runners but still requires bounded execution', async () => {
|
|
701
|
+
const root = await createWorkspace({
|
|
702
|
+
rootName: '@smoothbricks/codebase',
|
|
703
|
+
packages: [{ dir: 'app', name: '@smoothbricks/app', scripts: { test: 'node --test' } }],
|
|
704
|
+
});
|
|
705
|
+
try {
|
|
706
|
+
expect(validateWorkspaceDependencies(root)).toBe(1);
|
|
707
|
+
} finally {
|
|
708
|
+
await rm(root, { recursive: true, force: true });
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('detects bun test commands configured directly in Nx targets', async () => {
|
|
713
|
+
const root = await createWorkspace({
|
|
714
|
+
rootName: '@smoothbricks/codebase',
|
|
715
|
+
packages: [
|
|
716
|
+
{
|
|
717
|
+
dir: 'app',
|
|
718
|
+
name: '@smoothbricks/app',
|
|
719
|
+
nx: {
|
|
720
|
+
name: 'app',
|
|
721
|
+
targets: {
|
|
722
|
+
test: { executor: 'nx:run-commands', options: { command: 'bun test', cwd: '{projectRoot}' } },
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
});
|
|
728
|
+
try {
|
|
729
|
+
expect(validateWorkspaceDependencies(root)).toBe(2);
|
|
730
|
+
|
|
731
|
+
applyWorkspaceDependencyDefaults(root);
|
|
732
|
+
|
|
733
|
+
const testTsconfig = await readJson(join(root, 'packages/app/tsconfig.test.json'));
|
|
734
|
+
expect(testTsconfig.compilerOptions).toMatchObject({ noEmit: true, types: ['bun'] });
|
|
735
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
736
|
+
} finally {
|
|
737
|
+
await rm(root, { recursive: true, force: true });
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('accepts noEmit test tsconfig and build.zig with an explicit step', async () => {
|
|
742
|
+
const root = await createWorkspace({
|
|
743
|
+
rootName: '@smoothbricks/codebase',
|
|
744
|
+
packages: [{ dir: 'native', name: '@smoothbricks/native', nx: { name: 'native', targets: { lint: {} } } }],
|
|
745
|
+
});
|
|
746
|
+
try {
|
|
747
|
+
await writeJson(join(root, 'packages/native/tsconfig.test.json'), { compilerOptions: { noEmit: true } });
|
|
748
|
+
await writeFile(
|
|
749
|
+
join(root, 'packages/native/build.zig'),
|
|
750
|
+
'pub fn build(b: *std.Build) void { _ = b.step("build", "Build native artifact"); }\n',
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
754
|
+
} finally {
|
|
755
|
+
await rm(root, { recursive: true, force: true });
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('requires a test script or target when test files exist', async () => {
|
|
760
|
+
const root = await createWorkspace({
|
|
761
|
+
rootName: '@smoothbricks/codebase',
|
|
762
|
+
packages: [{ dir: 'app', name: '@smoothbricks/app' }],
|
|
763
|
+
});
|
|
764
|
+
try {
|
|
765
|
+
await mkdir(join(root, 'packages/app/src'), { recursive: true });
|
|
766
|
+
await writeFile(join(root, 'packages/app/src/example.test.ts'), 'export const testValue = true;\n');
|
|
767
|
+
|
|
768
|
+
expect(validateWorkspaceDependencies(root)).toBe(1);
|
|
769
|
+
} finally {
|
|
770
|
+
await rm(root, { recursive: true, force: true });
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('rewrites safe scripts that use workspace dependencies into Nx aliases', async () => {
|
|
775
|
+
const root = await createWorkspace({
|
|
776
|
+
rootName: '@smoothbricks/codebase',
|
|
777
|
+
packages: [
|
|
778
|
+
{ dir: 'lib', name: '@smoothbricks/lib', nx: { name: 'lib' } },
|
|
779
|
+
{
|
|
780
|
+
dir: 'app',
|
|
781
|
+
name: '@smoothbricks/app',
|
|
782
|
+
dependencies: { '@smoothbricks/lib': '^0.0.0' },
|
|
783
|
+
scripts: {
|
|
784
|
+
dev: 'astro dev',
|
|
785
|
+
serve: 'vite dev --host 0.0.0.0',
|
|
786
|
+
test: 'bun test --pass-with-no-tests',
|
|
787
|
+
build: 'wrangler build --outdir dist',
|
|
788
|
+
deploy: 'wrangler deploy',
|
|
789
|
+
astro: 'astro',
|
|
790
|
+
},
|
|
791
|
+
nx: { name: 'app' },
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
});
|
|
795
|
+
try {
|
|
796
|
+
expect(validateWorkspaceDependencies(root)).toBe(7);
|
|
797
|
+
|
|
798
|
+
applyWorkspaceDependencyDefaults(root);
|
|
799
|
+
|
|
800
|
+
const app = await readJson(join(root, 'packages/app/package.json'));
|
|
801
|
+
expect(app.dependencies).toEqual({ '@smoothbricks/lib': 'workspace:*' });
|
|
802
|
+
expect(app.scripts).toEqual({
|
|
803
|
+
dev: 'nx run app:dev --tui=false --outputStyle=stream',
|
|
804
|
+
serve: 'nx run app:serve --tui=false --outputStyle=stream',
|
|
805
|
+
test: 'nx run app:test --tui=false --outputStyle=stream',
|
|
806
|
+
build: 'nx run app:build',
|
|
807
|
+
deploy: 'wrangler deploy',
|
|
808
|
+
astro: 'astro',
|
|
809
|
+
});
|
|
810
|
+
expect(app.nx).toEqual({
|
|
811
|
+
name: 'app',
|
|
812
|
+
targets: {
|
|
813
|
+
dev: {
|
|
814
|
+
executor: 'nx:run-commands',
|
|
815
|
+
dependsOn: ['^build'],
|
|
816
|
+
continuous: true,
|
|
817
|
+
options: { command: 'astro dev', cwd: '{projectRoot}' },
|
|
818
|
+
},
|
|
819
|
+
serve: {
|
|
820
|
+
executor: 'nx:run-commands',
|
|
821
|
+
dependsOn: ['^build'],
|
|
822
|
+
continuous: true,
|
|
823
|
+
options: { command: 'vite dev --host 0.0.0.0', cwd: '{projectRoot}' },
|
|
824
|
+
},
|
|
825
|
+
test: {
|
|
826
|
+
executor: BOUNDED_TEST_EXECUTOR,
|
|
827
|
+
options: {
|
|
828
|
+
command: 'bun test --pass-with-no-tests',
|
|
829
|
+
cwd: '{projectRoot}',
|
|
830
|
+
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
831
|
+
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
build: {
|
|
835
|
+
executor: 'nx:run-commands',
|
|
836
|
+
dependsOn: ['^build'],
|
|
837
|
+
options: { command: 'wrangler build --outdir dist', cwd: '{projectRoot}' },
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
842
|
+
} finally {
|
|
843
|
+
await rm(root, { recursive: true, force: true });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('rejects Nx target commands that recurse through package scripts', async () => {
|
|
848
|
+
const root = await createWorkspace({
|
|
849
|
+
rootName: '@smoothbricks/codebase',
|
|
850
|
+
packages: [
|
|
851
|
+
{ dir: 'lib', name: '@smoothbricks/lib', nx: { name: 'lib' } },
|
|
852
|
+
{
|
|
853
|
+
dir: 'app',
|
|
854
|
+
name: '@smoothbricks/app',
|
|
855
|
+
dependencies: { '@smoothbricks/lib': 'workspace:*' },
|
|
856
|
+
scripts: { test: 'nx run app:test --tui=false --outputStyle=stream' },
|
|
857
|
+
nx: {
|
|
858
|
+
name: 'app',
|
|
859
|
+
targets: {
|
|
860
|
+
test: {
|
|
861
|
+
executor: BOUNDED_TEST_EXECUTOR,
|
|
862
|
+
options: {
|
|
863
|
+
command: 'bun run test',
|
|
864
|
+
cwd: '{projectRoot}',
|
|
865
|
+
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
866
|
+
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
});
|
|
874
|
+
try {
|
|
875
|
+
expect(validateWorkspaceDependencies(root)).toBe(1);
|
|
876
|
+
} finally {
|
|
877
|
+
await rm(root, { recursive: true, force: true });
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('moves simple leading environment assignments into Nx target options', async () => {
|
|
882
|
+
const root = await createWorkspace({
|
|
883
|
+
rootName: '@smoothbricks/codebase',
|
|
884
|
+
packages: [
|
|
885
|
+
{ dir: 'lib', name: '@smoothbricks/lib', nx: { name: 'lib' } },
|
|
886
|
+
{
|
|
887
|
+
dir: 'website',
|
|
888
|
+
name: '@smoothbricks/website',
|
|
889
|
+
dependencies: { '@smoothbricks/lib': 'workspace:*' },
|
|
890
|
+
scripts: {
|
|
891
|
+
dev: "NODE_OPTIONS='--import=extensionless/register' astro dev",
|
|
892
|
+
build: 'astro build',
|
|
893
|
+
preview: "NODE_OPTIONS='--import=extensionless/register' astro preview",
|
|
894
|
+
astro: 'astro',
|
|
895
|
+
},
|
|
896
|
+
nx: { name: 'website' },
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
});
|
|
900
|
+
try {
|
|
901
|
+
applyWorkspaceDependencyDefaults(root);
|
|
902
|
+
|
|
903
|
+
const website = await readJson(join(root, 'packages/website/package.json'));
|
|
904
|
+
expect(website.scripts).toEqual({
|
|
905
|
+
dev: 'nx run website:dev --tui=false --outputStyle=stream',
|
|
906
|
+
build: 'nx run website:build',
|
|
907
|
+
preview: 'nx run website:preview --tui=false --outputStyle=stream',
|
|
908
|
+
astro: 'astro',
|
|
909
|
+
});
|
|
910
|
+
expect(website.nx).toEqual({
|
|
911
|
+
name: 'website',
|
|
912
|
+
targets: {
|
|
913
|
+
dev: {
|
|
914
|
+
executor: 'nx:run-commands',
|
|
915
|
+
dependsOn: ['^build'],
|
|
916
|
+
continuous: true,
|
|
917
|
+
options: {
|
|
918
|
+
command: 'astro dev',
|
|
919
|
+
cwd: '{projectRoot}',
|
|
920
|
+
env: { NODE_OPTIONS: '--import=extensionless/register' },
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
build: {
|
|
924
|
+
executor: 'nx:run-commands',
|
|
925
|
+
dependsOn: ['^build'],
|
|
926
|
+
options: { command: 'astro build', cwd: '{projectRoot}' },
|
|
927
|
+
},
|
|
928
|
+
preview: {
|
|
929
|
+
executor: 'nx:run-commands',
|
|
930
|
+
dependsOn: ['build'],
|
|
931
|
+
continuous: true,
|
|
932
|
+
options: {
|
|
933
|
+
command: 'astro preview',
|
|
934
|
+
cwd: '{projectRoot}',
|
|
935
|
+
env: { NODE_OPTIONS: '--import=extensionless/register' },
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
941
|
+
} finally {
|
|
942
|
+
await rm(root, { recursive: true, force: true });
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('materializes nx.name when rewriting scripts for packages with existing nx config', async () => {
|
|
947
|
+
const root = await createWorkspace({
|
|
948
|
+
rootName: '@smoothbricks/codebase',
|
|
949
|
+
packages: [
|
|
950
|
+
{ dir: 'lib', name: '@smoothbricks/lib', nx: { name: 'lib' } },
|
|
951
|
+
{
|
|
952
|
+
dir: 'app',
|
|
953
|
+
name: '@external/app',
|
|
954
|
+
dependencies: { '@smoothbricks/lib': 'workspace:*' },
|
|
955
|
+
scripts: { test: 'bun test --pass-with-no-tests' },
|
|
956
|
+
nx: { targets: {} },
|
|
957
|
+
},
|
|
958
|
+
],
|
|
959
|
+
});
|
|
960
|
+
try {
|
|
961
|
+
expect(validateWorkspaceDependencies(root)).toBe(3);
|
|
962
|
+
|
|
963
|
+
applyWorkspaceDependencyDefaults(root);
|
|
964
|
+
|
|
965
|
+
const app = await readJson(join(root, 'packages/app/package.json'));
|
|
966
|
+
expect(app.scripts).toEqual({ test: 'nx run @external/app:test --tui=false --outputStyle=stream' });
|
|
967
|
+
expect(app.nx).toEqual({
|
|
968
|
+
name: '@external/app',
|
|
969
|
+
targets: {
|
|
970
|
+
test: {
|
|
971
|
+
executor: BOUNDED_TEST_EXECUTOR,
|
|
972
|
+
options: {
|
|
973
|
+
command: 'bun test --pass-with-no-tests',
|
|
974
|
+
cwd: '{projectRoot}',
|
|
975
|
+
timeoutMs: BOUNDED_TEST_TIMEOUT_MS,
|
|
976
|
+
killAfterMs: BOUNDED_TEST_KILL_AFTER_MS,
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
expect(validateWorkspaceDependencies(root)).toBe(0);
|
|
982
|
+
} finally {
|
|
983
|
+
await rm(root, { recursive: true, force: true });
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
async function createWorkspace(input: {
|
|
989
|
+
rootName: string;
|
|
990
|
+
packages: Array<{
|
|
991
|
+
dir: string;
|
|
992
|
+
name: string;
|
|
993
|
+
private?: boolean;
|
|
994
|
+
version?: string | null;
|
|
995
|
+
dependencies?: Record<string, string>;
|
|
996
|
+
scripts?: Record<string, string>;
|
|
997
|
+
nx?: Record<string, unknown>;
|
|
998
|
+
}>;
|
|
999
|
+
}): Promise<string> {
|
|
1000
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
1001
|
+
await writeJson(join(root, 'package.json'), {
|
|
1002
|
+
name: input.rootName,
|
|
1003
|
+
version: '0.0.0',
|
|
1004
|
+
private: true,
|
|
1005
|
+
workspaces: ['packages/*'],
|
|
1006
|
+
});
|
|
1007
|
+
for (const pkg of input.packages) {
|
|
1008
|
+
await writeJson(join(root, `packages/${pkg.dir}/package.json`), {
|
|
1009
|
+
name: pkg.name,
|
|
1010
|
+
...(pkg.version === null ? {} : { version: pkg.version ?? '0.0.0' }),
|
|
1011
|
+
...(pkg.private === undefined ? {} : { private: pkg.private }),
|
|
1012
|
+
...(pkg.dependencies ? { dependencies: pkg.dependencies } : {}),
|
|
1013
|
+
...(pkg.scripts ? { scripts: pkg.scripts } : {}),
|
|
1014
|
+
...(pkg.nx ? { nx: pkg.nx } : {}),
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
return root;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function readJson(path: string): Promise<Record<string, unknown>> {
|
|
1021
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const originalConsoleError = console.error;
|
|
1025
|
+
|
|
1026
|
+
function captureConsoleErrors(): string[] {
|
|
1027
|
+
const errors: string[] = [];
|
|
1028
|
+
spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
|
|
1029
|
+
errors.push(args.join(' '));
|
|
1030
|
+
});
|
|
1031
|
+
return errors;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function validRootPackage(extra: Record<string, unknown> = {}): Record<string, unknown> {
|
|
1035
|
+
return {
|
|
1036
|
+
name: '@smoothbricks/codebase',
|
|
1037
|
+
version: '0.0.0',
|
|
1038
|
+
private: true,
|
|
1039
|
+
license: 'MIT',
|
|
1040
|
+
repository: { type: 'git', url: 'git+https://github.com/smoothbricks/codebase.git' },
|
|
1041
|
+
packageManager: 'bun@1.3.13',
|
|
1042
|
+
engines: { node: '>=24.0.0' },
|
|
1043
|
+
devDependencies: { '@types/bun': '1.3.13' },
|
|
1044
|
+
nx: { includedScripts: [] },
|
|
1045
|
+
workspaces: ['packages/*'],
|
|
1046
|
+
...extra,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function validNxJson(): Record<string, unknown> {
|
|
1051
|
+
return {
|
|
1052
|
+
namedInputs: {
|
|
1053
|
+
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
1054
|
+
production: ['default'],
|
|
1055
|
+
sharedGlobals: ['{workspaceRoot}/.github/workflows/ci.yml'],
|
|
1056
|
+
},
|
|
1057
|
+
targetDefaults: {
|
|
1058
|
+
'lint:fix': { executor: 'nx:run-commands' },
|
|
1059
|
+
},
|
|
1060
|
+
plugins: [
|
|
1061
|
+
{
|
|
1062
|
+
plugin: '@nx/js/typescript',
|
|
1063
|
+
options: {
|
|
1064
|
+
typecheck: { targetName: 'typecheck' },
|
|
1065
|
+
build: { targetName: 'build', configName: 'tsconfig.lib.json' },
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
release: {
|
|
1070
|
+
projectsRelationship: 'independent',
|
|
1071
|
+
version: {
|
|
1072
|
+
specifierSource: 'conventional-commits',
|
|
1073
|
+
currentVersionResolver: 'git-tag',
|
|
1074
|
+
fallbackCurrentVersionResolver: 'disk',
|
|
1075
|
+
versionActions: '@smoothbricks/nx-plugin/version-actions',
|
|
1076
|
+
},
|
|
1077
|
+
releaseTag: { pattern: '{projectName}@{version}' },
|
|
1078
|
+
changelog: {
|
|
1079
|
+
workspaceChangelog: false,
|
|
1080
|
+
projectChangelogs: {
|
|
1081
|
+
createRelease: false,
|
|
1082
|
+
file: false,
|
|
1083
|
+
renderOptions: { authors: true, applyUsernameToAuthors: true },
|
|
1084
|
+
},
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function validConfiguredNxJson(): Record<string, unknown> {
|
|
1091
|
+
return {
|
|
1092
|
+
...validNxJson(),
|
|
1093
|
+
namedInputs: validNamedInputs(),
|
|
1094
|
+
targetDefaults: { build: { cache: true, outputs: ['{projectRoot}/dist'] } },
|
|
1095
|
+
plugins: [
|
|
1096
|
+
{
|
|
1097
|
+
plugin: '@nx/js/typescript',
|
|
1098
|
+
options: {
|
|
1099
|
+
typecheck: { targetName: 'typecheck' },
|
|
1100
|
+
build: {
|
|
1101
|
+
targetName: 'tsc-js',
|
|
1102
|
+
configName: 'tsconfig.lib.json',
|
|
1103
|
+
buildDepsName: 'build-deps',
|
|
1104
|
+
watchDepsName: 'watch-deps',
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
'@smoothbricks/nx-plugin',
|
|
1109
|
+
],
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function validNamedInputs(): Record<string, unknown> {
|
|
1114
|
+
return {
|
|
1115
|
+
default: ['{projectRoot}/**/*', 'sharedGlobals'],
|
|
1116
|
+
production: [
|
|
1117
|
+
'{projectRoot}/src/**/*',
|
|
1118
|
+
'{projectRoot}/package.json',
|
|
1119
|
+
'!{projectRoot}/**/__tests__/**',
|
|
1120
|
+
'!{projectRoot}/**/*.test.*',
|
|
1121
|
+
'!{projectRoot}/**/*.spec.*',
|
|
1122
|
+
],
|
|
1123
|
+
sharedGlobals: ['{workspaceRoot}/.github/workflows/ci.yml'],
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async function writeJson(path: string, value: Record<string, unknown>): Promise<void> {
|
|
1128
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1129
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
1130
|
+
}
|