@smoothbricks/cli 0.10.0 → 0.10.2
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/dist/cli.js +5 -2
- package/dist/github-ci/index.js +4 -3
- package/dist/lib/conflict-markers.d.ts.map +1 -1
- package/dist/lib/workspace.d.ts +1 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +1 -0
- package/dist/monorepo/lockfile.d.ts +2 -0
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +13 -2
- package/dist/monorepo/managed-files.d.ts +1 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +12 -5
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +1 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +20 -7
- package/dist/release/github-release.d.ts +2 -2
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +1 -1
- package/dist/wrangler/scaffold.d.ts.map +1 -1
- package/dist/wrangler/scaffold.js +1 -1
- package/managed/raw/tooling/direnv/setup-environment.ts +2 -8
- package/managed/raw/tooling/git-hooks/pre-commit.sh +6 -0
- package/package.json +3 -13
- package/src/cli.ts +7 -4
- package/src/github-ci/index.ts +5 -3
- package/src/monorepo/__tests__/ci-workflow.test.ts +2 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +12 -4
- package/src/monorepo/ci-workflow.ts +2 -0
- package/src/monorepo/lockfile.test.ts +157 -0
- package/src/monorepo/lockfile.ts +15 -2
- package/src/monorepo/managed-files.test.ts +13 -0
- package/src/monorepo/managed-files.ts +20 -5
- package/src/monorepo/package-policy.test.ts +7 -37
- package/src/monorepo/publish-workflow.ts +2 -0
- package/src/monorepo/tool-validation.test.ts +59 -24
- package/src/monorepo/tool-validation.ts +21 -7
- package/src/monorepo/wrangler.test.ts +9 -2
- package/src/release/__tests__/fixture-repo.test.ts +8 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +28 -8
- package/src/wrangler/prepare-env.ts +14 -4
- package/src/wrangler/scaffold.ts +8 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { afterAll, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import fc from 'fast-check';
|
|
7
|
+
import { syncBunLockfileVersions, validateBunLockfileVersions } from './lockfile.js';
|
|
8
|
+
|
|
9
|
+
const cleanupRoots: string[] = [];
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
for (const root of cleanupRoots) {
|
|
12
|
+
rmSync(root, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function git(root: string, args: string): string {
|
|
17
|
+
return execSync(`git ${args}`, {
|
|
18
|
+
cwd: root,
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
23
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
24
|
+
GIT_AUTHOR_NAME: 'fixture',
|
|
25
|
+
GIT_AUTHOR_EMAIL: 'fixture@invalid',
|
|
26
|
+
GIT_COMMITTER_NAME: 'fixture',
|
|
27
|
+
GIT_COMMITTER_EMAIL: 'fixture@invalid',
|
|
28
|
+
},
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FixtureOptions {
|
|
34
|
+
packageVersion: string;
|
|
35
|
+
lockVersion: string;
|
|
36
|
+
stableTag?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeFixtureRepo(options: FixtureOptions): string {
|
|
40
|
+
const root = mkdtempSync(join(tmpdir(), 'smoo-lockfile-'));
|
|
41
|
+
cleanupRoots.push(root);
|
|
42
|
+
git(root, 'init -q');
|
|
43
|
+
git(root, 'commit -q --allow-empty -m init');
|
|
44
|
+
writeFileSync(
|
|
45
|
+
join(root, 'package.json'),
|
|
46
|
+
JSON.stringify({ name: 'fixture-root', version: '0.0.0', private: true, workspaces: ['packages/*'] }),
|
|
47
|
+
);
|
|
48
|
+
mkdirSync(join(root, 'packages', 'foo'), { recursive: true });
|
|
49
|
+
writeFixturePackage(root, options.packageVersion);
|
|
50
|
+
writeFixtureLock(root, options.lockVersion);
|
|
51
|
+
if (options.stableTag) {
|
|
52
|
+
git(root, `tag ${options.stableTag}`);
|
|
53
|
+
}
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeFixturePackage(root: string, version: string): void {
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(root, 'packages', 'foo', 'package.json'),
|
|
60
|
+
JSON.stringify({ name: '@fixture/foo', version, nx: { name: 'foo' } }),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeFixtureLock(root: string, version: string): void {
|
|
65
|
+
writeFileSync(
|
|
66
|
+
join(root, 'bun.lock'),
|
|
67
|
+
[
|
|
68
|
+
'{',
|
|
69
|
+
' "lockfileVersion": 1,',
|
|
70
|
+
' "workspaces": {',
|
|
71
|
+
' "": { "name": "fixture-root" },',
|
|
72
|
+
' "packages/foo": {',
|
|
73
|
+
' "name": "@fixture/foo",',
|
|
74
|
+
` "version": "${version}",`,
|
|
75
|
+
' },',
|
|
76
|
+
' },',
|
|
77
|
+
'}',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n'),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function lockVersionIn(root: string): string {
|
|
84
|
+
const match = readFileSync(join(root, 'bun.lock'), 'utf8').match(/"packages\/foo":\s*\{[^}]*"version":\s*"([^"]+)"/);
|
|
85
|
+
if (!match) throw new Error('fixture lockfile lost its workspace entry');
|
|
86
|
+
return match[1] as string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('bun.lock workspace version sync', () => {
|
|
90
|
+
it('a bun-install revert of the release-synced version fails validation', () => {
|
|
91
|
+
// The b3298fc4c shape: package.json holds the next prerelease, a stable tag
|
|
92
|
+
// exists, and `bun install` rewrote the lockfile back to the prerelease.
|
|
93
|
+
const root = makeFixtureRepo({
|
|
94
|
+
packageVersion: '0.1.3-next.0',
|
|
95
|
+
lockVersion: '0.1.3-next.0',
|
|
96
|
+
stableTag: 'foo@0.1.2',
|
|
97
|
+
});
|
|
98
|
+
expect(validateBunLockfileVersions(root)).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('sync restores the latest stable tag version and validation passes', () => {
|
|
102
|
+
const root = makeFixtureRepo({
|
|
103
|
+
packageVersion: '0.1.3-next.0',
|
|
104
|
+
lockVersion: '0.1.3-next.0',
|
|
105
|
+
stableTag: 'foo@0.1.2',
|
|
106
|
+
});
|
|
107
|
+
expect(syncBunLockfileVersions(root, { log: false })).toBe(1);
|
|
108
|
+
expect(lockVersionIn(root)).toBe('0.1.2');
|
|
109
|
+
expect(validateBunLockfileVersions(root)).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('a never-published prerelease package falls back to its manifest version', () => {
|
|
113
|
+
// The new-workspace-package shape (e.g. cowshed): no stable tag exists yet.
|
|
114
|
+
const root = makeFixtureRepo({ packageVersion: '0.2.0-next.0', lockVersion: '0.2.0-next.0' });
|
|
115
|
+
expect(validateBunLockfileVersions(root)).toBe(0);
|
|
116
|
+
expect(syncBunLockfileVersions(root, { log: false })).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('a stable manifest version is required verbatim in the lockfile', () => {
|
|
120
|
+
const root = makeFixtureRepo({ packageVersion: '0.1.0', lockVersion: '0.0.9' });
|
|
121
|
+
expect(validateBunLockfileVersions(root)).toBe(1);
|
|
122
|
+
expect(syncBunLockfileVersions(root, { log: false })).toBe(1);
|
|
123
|
+
expect(lockVersionIn(root)).toBe('0.1.0');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('stage: true stages the healed bun.lock', () => {
|
|
127
|
+
const root = makeFixtureRepo({
|
|
128
|
+
packageVersion: '0.1.3-next.0',
|
|
129
|
+
lockVersion: '0.1.3-next.0',
|
|
130
|
+
stableTag: 'foo@0.1.2',
|
|
131
|
+
});
|
|
132
|
+
expect(syncBunLockfileVersions(root, { log: false, stage: true })).toBe(1);
|
|
133
|
+
expect(git(root, 'diff --cached --name-only')).toContain('bun.lock');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('stage: true leaves a clean lockfile unstaged', () => {
|
|
137
|
+
const root = makeFixtureRepo({ packageVersion: '0.1.2', lockVersion: '0.1.2', stableTag: 'foo@0.1.2' });
|
|
138
|
+
expect(syncBunLockfileVersions(root, { log: false, stage: true })).toBe(0);
|
|
139
|
+
expect(git(root, 'diff --cached --name-only').trim()).toBe('');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('property: after sync, validation always passes', () => {
|
|
143
|
+
const version = fc
|
|
144
|
+
.tuple(fc.nat({ max: 20 }), fc.nat({ max: 20 }), fc.nat({ max: 20 }), fc.option(fc.nat({ max: 5 })))
|
|
145
|
+
.map(([major, minor, patch, next]) => `${major}.${minor}.${patch}${next === null ? '' : `-next.${next}`}`);
|
|
146
|
+
const root = makeFixtureRepo({ packageVersion: '0.1.2', lockVersion: '0.1.2', stableTag: 'foo@0.1.2' });
|
|
147
|
+
fc.assert(
|
|
148
|
+
fc.property(version, version, (packageVersion, lockVersion) => {
|
|
149
|
+
writeFixturePackage(root, packageVersion);
|
|
150
|
+
writeFixtureLock(root, lockVersion);
|
|
151
|
+
syncBunLockfileVersions(root, { log: false });
|
|
152
|
+
return validateBunLockfileVersions(root) === 0;
|
|
153
|
+
}),
|
|
154
|
+
{ numRuns: 25 },
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
});
|
package/src/monorepo/lockfile.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
|
|
|
5
5
|
|
|
6
6
|
export interface SyncBunLockfileVersionsOptions {
|
|
7
7
|
log?: boolean;
|
|
8
|
+
/** `git add` bun.lock after rewriting it, for the pre-commit self-heal path. */
|
|
9
|
+
stage?: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
// Temporary Bun workaround. Delete this sync function, validateBunLockfileVersions,
|
|
@@ -16,6 +18,12 @@ export interface SyncBunLockfileVersionsOptions {
|
|
|
16
18
|
// - https://github.com/oven-sh/bun/issues/18906
|
|
17
19
|
// - https://github.com/oven-sh/bun/issues/20477
|
|
18
20
|
// - https://github.com/oven-sh/bun/issues/20829
|
|
21
|
+
//
|
|
22
|
+
// Any `bun install` rewrites the workspace versions from package.json, reverting
|
|
23
|
+
// this sync between releases. The pre-commit hook therefore runs
|
|
24
|
+
// `smoo monorepo sync-bun-lockfile-versions --stage` on every commit so drift is
|
|
25
|
+
// healed at the commit that would otherwise carry it — validation must never
|
|
26
|
+
// fail a committer for drift a routine `bun install` introduced.
|
|
19
27
|
export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVersionsOptions = {}): number {
|
|
20
28
|
const log = options.log ?? true;
|
|
21
29
|
const lockfilePath = join(root, 'bun.lock');
|
|
@@ -59,10 +67,15 @@ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVe
|
|
|
59
67
|
}
|
|
60
68
|
if (updated > 0) {
|
|
61
69
|
writeFileSync(lockfilePath, lockfile);
|
|
70
|
+
if (options.stage) {
|
|
71
|
+
execSync('git add bun.lock', { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
72
|
+
}
|
|
62
73
|
}
|
|
63
|
-
if (log) {
|
|
74
|
+
if (log || updated > 0) {
|
|
64
75
|
console.log(
|
|
65
|
-
updated > 0
|
|
76
|
+
updated > 0
|
|
77
|
+
? `Updated ${updated} workspace version(s) in bun.lock${options.stage ? ' (staged)' : ''}`
|
|
78
|
+
: 'All workspace versions already in sync.',
|
|
66
79
|
);
|
|
67
80
|
}
|
|
68
81
|
return updated;
|
|
@@ -93,6 +93,19 @@ describe('managed-file inline local blocks', () => {
|
|
|
93
93
|
);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
it('preserves marker indentation when refreshing nested configuration', () => {
|
|
97
|
+
const markerIndent = ' ';
|
|
98
|
+
const current = [
|
|
99
|
+
'patterns:',
|
|
100
|
+
" - '*.html'",
|
|
101
|
+
`${markerIndent}${INLINE_LOCAL_BEGIN}`,
|
|
102
|
+
" - '!**/templates/*.html'",
|
|
103
|
+
`${markerIndent}${INLINE_LOCAL_END}`,
|
|
104
|
+
].join('\n');
|
|
105
|
+
const { withoutInline, blocks } = extractInlineLocalBlocksForTest(current);
|
|
106
|
+
expect(reinsertInlineLocalBlocksForTest(withoutInline, blocks)).toBe(current);
|
|
107
|
+
});
|
|
108
|
+
|
|
96
109
|
it("reinserting refuses when the anchor no longer appears — never silently drops the repo's customization", () => {
|
|
97
110
|
const fresh = ['a:', ' - one', 'b:'].join('\n'); // ' - two' is gone
|
|
98
111
|
expect(() => reinsertInlineLocalBlocksForTest(fresh, [{ anchor: ' - two', lines: ' - repo-owned' }])).toThrow(
|
|
@@ -50,6 +50,7 @@ export const INLINE_LOCAL_END = '# smoo-local-end';
|
|
|
50
50
|
interface InlineLocalBlock {
|
|
51
51
|
anchor: string;
|
|
52
52
|
lines: string;
|
|
53
|
+
markerIndent?: string;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Pull inline local blocks out of a managed section, returning the section
|
|
@@ -76,7 +77,12 @@ function extractInlineLocalBlocks(managed: string): { withoutInline: string; blo
|
|
|
76
77
|
if (i >= lines.length) {
|
|
77
78
|
throw new Error(`${INLINE_LOCAL_BEGIN} anchored on "${anchor}" has no matching ${INLINE_LOCAL_END}`);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
const markerIndent = line.slice(0, line.length - line.trimStart().length);
|
|
81
|
+
blocks.push({
|
|
82
|
+
anchor,
|
|
83
|
+
lines: blockLines.join('\n'),
|
|
84
|
+
...(markerIndent === '' ? {} : { markerIndent }),
|
|
85
|
+
});
|
|
80
86
|
i += 1; // skip the END marker line itself
|
|
81
87
|
continue;
|
|
82
88
|
}
|
|
@@ -102,7 +108,14 @@ function reinsertInlineLocalBlocks(content: string, blocks: InlineLocalBlock[]):
|
|
|
102
108
|
'template — reconcile the repo-owned block manually',
|
|
103
109
|
);
|
|
104
110
|
}
|
|
105
|
-
|
|
111
|
+
const markerIndent = block.markerIndent ?? '';
|
|
112
|
+
lines.splice(
|
|
113
|
+
index + 1,
|
|
114
|
+
0,
|
|
115
|
+
`${markerIndent}${INLINE_LOCAL_BEGIN}`,
|
|
116
|
+
...block.lines.split('\n'),
|
|
117
|
+
`${markerIndent}${INLINE_LOCAL_END}`,
|
|
118
|
+
);
|
|
106
119
|
}
|
|
107
120
|
return lines.join('\n');
|
|
108
121
|
}
|
|
@@ -388,9 +401,11 @@ function readCiPushBranches(packageJson: unknown): string[] {
|
|
|
388
401
|
}
|
|
389
402
|
|
|
390
403
|
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
391
|
-
return value
|
|
392
|
-
|
|
393
|
-
|
|
404
|
+
return isRecord(value) ? value : undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
408
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
394
409
|
}
|
|
395
410
|
|
|
396
411
|
function renderYamlFlowList(values: string[]): string {
|
|
@@ -70,21 +70,7 @@ describe('root smoo monorepo policy', () => {
|
|
|
70
70
|
expect(rootPackage).toMatchObject({ nx: { includedScripts: [] } });
|
|
71
71
|
expect(nxJson.namedInputs).toEqual(validNamedInputs());
|
|
72
72
|
expect(nxJson.targetDefaults).toEqual(validTargetDefaults());
|
|
73
|
-
expect(nxJson.plugins).toEqual([
|
|
74
|
-
{
|
|
75
|
-
plugin: '@nx/js/typescript',
|
|
76
|
-
options: {
|
|
77
|
-
typecheck: { targetName: 'typecheck' },
|
|
78
|
-
build: {
|
|
79
|
-
targetName: 'tsc-js',
|
|
80
|
-
configName: 'tsconfig.lib.json',
|
|
81
|
-
buildDepsName: 'build-deps',
|
|
82
|
-
watchDepsName: 'watch-deps',
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
'@smoothbricks/nx-plugin',
|
|
87
|
-
]);
|
|
73
|
+
expect(nxJson.plugins).toEqual(['@smoothbricks/nx-plugin']);
|
|
88
74
|
expect(validateRootPackagePolicy(root)).toBe(0);
|
|
89
75
|
expect(validateNxReleaseConfig(root)).toBe(0);
|
|
90
76
|
} finally {
|
|
@@ -167,7 +153,7 @@ describe('root smoo monorepo policy', () => {
|
|
|
167
153
|
}
|
|
168
154
|
});
|
|
169
155
|
|
|
170
|
-
it('explains Nx plugin conventions when nx.json is not configured', async () => {
|
|
156
|
+
it('explains SmoothBricks Nx plugin conventions when nx.json is not configured', async () => {
|
|
171
157
|
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
172
158
|
try {
|
|
173
159
|
await writeJson(join(root, 'package.json'), validRootPackage());
|
|
@@ -177,10 +163,8 @@ describe('root smoo monorepo policy', () => {
|
|
|
177
163
|
});
|
|
178
164
|
const errors = captureConsoleErrors();
|
|
179
165
|
|
|
180
|
-
expect(validateNxReleaseConfig(root)).toBe(
|
|
166
|
+
expect(validateNxReleaseConfig(root)).toBe(1);
|
|
181
167
|
|
|
182
|
-
expect(errors.join('\n')).toContain('Official Nx owns TypeScript library inference');
|
|
183
|
-
expect(errors.join('\n')).toContain('tsconfig.lib.json produces tsc-js');
|
|
184
168
|
expect(errors.join('\n')).toContain('Smoo relies on this plugin to infer convention targets');
|
|
185
169
|
expect(errors.join('\n')).not.toContain('Fix:');
|
|
186
170
|
} finally {
|
|
@@ -188,7 +172,7 @@ describe('root smoo monorepo policy', () => {
|
|
|
188
172
|
}
|
|
189
173
|
});
|
|
190
174
|
|
|
191
|
-
it('
|
|
175
|
+
it('rejects the official Nx TypeScript plugin because it bypasses ttsc transforms', async () => {
|
|
192
176
|
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
193
177
|
try {
|
|
194
178
|
await writeJson(join(root, 'package.json'), validRootPackage());
|
|
@@ -206,8 +190,8 @@ describe('root smoo monorepo policy', () => {
|
|
|
206
190
|
|
|
207
191
|
expect(validateNxReleaseConfig(root)).toBe(1);
|
|
208
192
|
|
|
209
|
-
expect(errors.join('\n')).toContain('
|
|
210
|
-
expect(errors.join('\n')).toContain('
|
|
193
|
+
expect(errors.join('\n')).toContain('plugins must not configure @nx/js/typescript');
|
|
194
|
+
expect(errors.join('\n')).toContain('Smoo owns transformer-aware TypeScript targets');
|
|
211
195
|
expect(errors.join('\n')).not.toContain('Fix:');
|
|
212
196
|
} finally {
|
|
213
197
|
await rm(root, { recursive: true, force: true });
|
|
@@ -1100,21 +1084,7 @@ function validConfiguredNxJson(): Record<string, unknown> {
|
|
|
1100
1084
|
...validNxJson(),
|
|
1101
1085
|
namedInputs: validNamedInputs(),
|
|
1102
1086
|
targetDefaults: validTargetDefaults(),
|
|
1103
|
-
plugins: [
|
|
1104
|
-
{
|
|
1105
|
-
plugin: '@nx/js/typescript',
|
|
1106
|
-
options: {
|
|
1107
|
-
typecheck: { targetName: 'typecheck' },
|
|
1108
|
-
build: {
|
|
1109
|
-
targetName: 'tsc-js',
|
|
1110
|
-
configName: 'tsconfig.lib.json',
|
|
1111
|
-
buildDepsName: 'build-deps',
|
|
1112
|
-
watchDepsName: 'watch-deps',
|
|
1113
|
-
},
|
|
1114
|
-
},
|
|
1115
|
-
},
|
|
1116
|
-
'@smoothbricks/nx-plugin',
|
|
1117
|
-
],
|
|
1087
|
+
plugins: ['@smoothbricks/nx-plugin'],
|
|
1118
1088
|
};
|
|
1119
1089
|
}
|
|
1120
1090
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* biome-ignore-all lint/suspicious/noTemplateCurlyInString: GitHub Actions expressions are emitted literally. */
|
|
2
|
+
|
|
1
3
|
import { isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
|
|
2
4
|
|
|
3
5
|
export type PublishWorkflowBump = 'auto' | 'patch' | 'minor' | 'major' | 'prerelease';
|
|
@@ -12,12 +12,13 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const registryVersions: Record<string, string[]> = {
|
|
14
14
|
'@biomejs/biome': ['2.3.5'],
|
|
15
|
-
'@nx/js': ['
|
|
16
|
-
'@smoothbricks/nx-plugin': ['0.0
|
|
15
|
+
'@nx/js': ['23.1.0'],
|
|
16
|
+
'@smoothbricks/nx-plugin': ['0.3.0'],
|
|
17
17
|
eslint: ['9.39.1'],
|
|
18
18
|
'eslint-stdout': ['1.1.1', '1.1.2'],
|
|
19
|
-
nx: ['
|
|
19
|
+
nx: ['23.1.0'],
|
|
20
20
|
prettier: ['3.6.1'],
|
|
21
|
+
ttsc: ['0.18.4'],
|
|
21
22
|
typescript: ['5.9.3'],
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -25,7 +26,7 @@ const realFetch = globalThis.fetch;
|
|
|
25
26
|
|
|
26
27
|
describe('tool configuration validation', () => {
|
|
27
28
|
beforeEach(() => {
|
|
28
|
-
globalThis.fetch = mockRegistryFetch
|
|
29
|
+
globalThis.fetch = mockRegistryFetch;
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
afterEach(() => {
|
|
@@ -67,7 +68,7 @@ describe('tool configuration validation', () => {
|
|
|
67
68
|
expect(rootPackage.devDependencies['@smoothbricks/cli']).toBeUndefined();
|
|
68
69
|
expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe('workspace:*');
|
|
69
70
|
expect(rootPackage.devDependencies['eslint-stdout']).toBe('workspace:*');
|
|
70
|
-
expect(rootPackage.devDependencies.nx).toBe('
|
|
71
|
+
expect(rootPackage.devDependencies.nx).toBe('23.1.0');
|
|
71
72
|
expect(rootPackage.workspaces).toContain('tooling');
|
|
72
73
|
expect(toolingPackage.name).toBe('@smoothbricks/tooling');
|
|
73
74
|
expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
|
|
@@ -89,13 +90,14 @@ describe('tool configuration validation', () => {
|
|
|
89
90
|
workspaces: ['packages/*', 'tooling'],
|
|
90
91
|
devDependencies: {
|
|
91
92
|
'@biomejs/biome': '^3.0.0',
|
|
92
|
-
'@nx/js': '
|
|
93
|
-
'@smoothbricks/nx-plugin': '^0.
|
|
93
|
+
'@nx/js': '23.2.0',
|
|
94
|
+
'@smoothbricks/nx-plugin': '^0.4.0',
|
|
94
95
|
'@types/bun': '1.3.99',
|
|
95
96
|
eslint: '^10.0.0',
|
|
96
97
|
'eslint-stdout': await currentEslintStdoutRange(),
|
|
97
|
-
nx: '
|
|
98
|
+
nx: '24.0.0',
|
|
98
99
|
prettier: '^3.7.0',
|
|
100
|
+
ttsc: '^0.19.0',
|
|
99
101
|
typescript: '^6.0.0',
|
|
100
102
|
},
|
|
101
103
|
});
|
|
@@ -132,6 +134,35 @@ describe('tool configuration validation', () => {
|
|
|
132
134
|
}
|
|
133
135
|
});
|
|
134
136
|
|
|
137
|
+
it('preserves an installable CLI range when the running CLI is a linked prerelease', async () => {
|
|
138
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
|
|
139
|
+
try {
|
|
140
|
+
const configuredRange = '^0.10.1';
|
|
141
|
+
const runningRange = await currentCliRange();
|
|
142
|
+
const expectedRange = runningRange.includes('-') ? configuredRange : runningRange;
|
|
143
|
+
await writeJson(join(root, 'package.json'), {
|
|
144
|
+
name: '@fixture/app',
|
|
145
|
+
version: '0.0.0',
|
|
146
|
+
private: true,
|
|
147
|
+
workspaces: ['packages/*', 'tooling'],
|
|
148
|
+
});
|
|
149
|
+
await writeJson(join(root, 'tooling/package.json'), {
|
|
150
|
+
name: '@fixture/tooling',
|
|
151
|
+
private: true,
|
|
152
|
+
dependencies: { '@smoothbricks/cli': configuredRange },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const context = readToolContext(root);
|
|
156
|
+
expect(context.policy.cliDependencyRange).toBe(expectedRange);
|
|
157
|
+
applyToolingPackageDefaults(root, context.policy);
|
|
158
|
+
|
|
159
|
+
const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
|
|
160
|
+
expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe(expectedRange);
|
|
161
|
+
} finally {
|
|
162
|
+
await rm(root, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
135
166
|
it('uses workspace smoo in the SmoothBricks codebase repo', async () => {
|
|
136
167
|
const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
|
|
137
168
|
try {
|
|
@@ -161,10 +192,11 @@ describe('tool configuration validation', () => {
|
|
|
161
192
|
workspaces: ['packages/*', 'tooling'],
|
|
162
193
|
devDependencies: {
|
|
163
194
|
'@biomejs/biome': '^3.0.0',
|
|
164
|
-
'@nx/js': '
|
|
195
|
+
'@nx/js': '23.2.0',
|
|
165
196
|
eslint: '^10.0.0',
|
|
166
|
-
nx: '
|
|
197
|
+
nx: '24.0.0',
|
|
167
198
|
prettier: '^3.7.0',
|
|
199
|
+
ttsc: '^0.19.0',
|
|
168
200
|
typescript: '^6.0.0',
|
|
169
201
|
},
|
|
170
202
|
});
|
|
@@ -195,20 +227,23 @@ async function currentEslintStdoutRange(): Promise<string> {
|
|
|
195
227
|
return `^${latestPatchVersion('eslint-stdout', pkg.version) ?? pkg.version}`;
|
|
196
228
|
}
|
|
197
229
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
230
|
+
const mockRegistryFetch = Object.assign(
|
|
231
|
+
(input: string | URL | Request): Promise<Response> => {
|
|
232
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
233
|
+
const packageName = decodeURIComponent(new URL(url).pathname.slice(1));
|
|
234
|
+
const versions = registryVersions[packageName];
|
|
235
|
+
if (!versions) {
|
|
236
|
+
return Promise.resolve(new Response('{}', { status: 404 }));
|
|
237
|
+
}
|
|
238
|
+
return Promise.resolve(
|
|
239
|
+
new Response(JSON.stringify({ versions: Object.fromEntries(versions.map((version) => [version, {}])) }), {
|
|
240
|
+
status: 200,
|
|
241
|
+
headers: { 'content-type': 'application/json' },
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
{ preconnect: realFetch.preconnect },
|
|
246
|
+
) satisfies typeof fetch;
|
|
212
247
|
|
|
213
248
|
function latestPatchVersion(packageName: string, version: string): string | null {
|
|
214
249
|
const [major, minor] = version.split('.');
|
|
@@ -25,11 +25,11 @@ export interface ToolContext {
|
|
|
25
25
|
|
|
26
26
|
const rootDevDependencies: RequiredDependency[] = [
|
|
27
27
|
{ name: '@biomejs/biome', fallbackVersion: '^2.3.5', minimumVersion: '2.3.0', prefix: '^' },
|
|
28
|
-
{ name: '@nx/js', fallbackVersion: '
|
|
28
|
+
{ name: '@nx/js', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
|
|
29
29
|
{
|
|
30
30
|
name: '@smoothbricks/nx-plugin',
|
|
31
|
-
fallbackVersion: '^0.0
|
|
32
|
-
minimumVersion: '0.0
|
|
31
|
+
fallbackVersion: '^0.3.0',
|
|
32
|
+
minimumVersion: '0.3.0',
|
|
33
33
|
prefix: '^',
|
|
34
34
|
useWorkspaceRangeInCodebase: true,
|
|
35
35
|
},
|
|
@@ -41,8 +41,11 @@ const rootDevDependencies: RequiredDependency[] = [
|
|
|
41
41
|
prefix: '^',
|
|
42
42
|
useWorkspaceRangeInCodebase: true,
|
|
43
43
|
},
|
|
44
|
-
{ name: 'nx', fallbackVersion: '
|
|
44
|
+
{ name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
|
|
45
45
|
{ name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', prefix: '^' },
|
|
46
|
+
{ name: 'ttsc', fallbackVersion: '^0.18.4', minimumVersion: '0.18.4', prefix: '^' },
|
|
47
|
+
// Nx and typescript-eslint still load the TypeScript 5.x JavaScript API.
|
|
48
|
+
// Compilation is exclusively delegated to ttsc by the Nx plugin targets.
|
|
46
49
|
{ name: 'typescript', fallbackVersion: '^5.9.3', minimumVersion: '5.9.0', prefix: '^' },
|
|
47
50
|
];
|
|
48
51
|
|
|
@@ -330,17 +333,28 @@ function formatExpectedDependency(policy: ToolPolicy, dependency: RequiredDepend
|
|
|
330
333
|
|
|
331
334
|
export function readToolContext(root: string): ToolContext {
|
|
332
335
|
const rootPackage = readPackageJsonObject(join(root, 'package.json'));
|
|
333
|
-
|
|
336
|
+
const toolingPackage = readJsonObject(join(root, 'tooling', 'package.json'));
|
|
337
|
+
const configuredCliRange = recordProperty(toolingPackage ?? {}, 'dependencies')?.[cliPackageName];
|
|
338
|
+
return {
|
|
339
|
+
rootPackage,
|
|
340
|
+
policy: toolPolicy(rootPackage, typeof configuredCliRange === 'string' ? configuredCliRange : null),
|
|
341
|
+
};
|
|
334
342
|
}
|
|
335
343
|
|
|
336
|
-
function toolPolicy(rootPackage: PackageJson | null): ToolPolicy {
|
|
344
|
+
function toolPolicy(rootPackage: PackageJson | null, configuredCliRange: string | null): ToolPolicy {
|
|
337
345
|
const name = typeof rootPackage?.name === 'string' ? rootPackage.name : null;
|
|
338
346
|
const isCodebase = isSmoothBricksCodebasePackageName(name ?? undefined);
|
|
339
347
|
const toolingName = toolingPackageName(name);
|
|
348
|
+
// A locally linked prerelease can be newer than every published CLI. Keep an existing
|
|
349
|
+
// installable range instead of rewriting the consumer manifest to an unpublished version.
|
|
350
|
+
const publishedCliRange =
|
|
351
|
+
cliPackageVersion.includes('-') && configuredCliRange && parseVersion(configuredCliRange)
|
|
352
|
+
? configuredCliRange
|
|
353
|
+
: `^${cliPackageVersion}`;
|
|
340
354
|
return {
|
|
341
355
|
isSmoothBricksCodebase: isCodebase,
|
|
342
356
|
toolingPackageName: toolingName,
|
|
343
|
-
cliDependencyRange: isCodebase ? 'workspace:*' :
|
|
357
|
+
cliDependencyRange: isCodebase ? 'workspace:*' : publishedCliRange,
|
|
344
358
|
};
|
|
345
359
|
}
|
|
346
360
|
|
|
@@ -21,7 +21,12 @@ describe('firstWranglerEnv', () => {
|
|
|
21
21
|
describe('applyWranglerDefaults', () => {
|
|
22
22
|
it('injects the wrangler-types target and wires typecheck, idempotently', async () => {
|
|
23
23
|
const root = await createWorkspace([
|
|
24
|
-
{
|
|
24
|
+
{
|
|
25
|
+
dir: 'api',
|
|
26
|
+
name: '@acme/api',
|
|
27
|
+
toml: '[env.production]\n',
|
|
28
|
+
nx: { targets: { typecheck: { dependsOn: ['^build'] } } },
|
|
29
|
+
},
|
|
25
30
|
]);
|
|
26
31
|
try {
|
|
27
32
|
applyWranglerDefaults(root);
|
|
@@ -51,7 +56,9 @@ describe('applyWranglerDefaults', () => {
|
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
it('omits --env from the command when the wrangler.toml declares no env block', async () => {
|
|
54
|
-
const root = await createWorkspace([
|
|
59
|
+
const root = await createWorkspace([
|
|
60
|
+
{ dir: 'api', name: '@acme/api', toml: 'name = "svc"\nmain = "src/index.ts"\n' },
|
|
61
|
+
]);
|
|
55
62
|
captureConsoleLogs();
|
|
56
63
|
try {
|
|
57
64
|
applyWranglerDefaults(root);
|
|
@@ -226,6 +226,14 @@ describe('release planning with fixture git repositories', () => {
|
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
it('kills timed-out fixture Git process groups so descendants cannot hold output pipes open', async () => {
|
|
230
|
+
await withFixtureRepo(async (root) => {
|
|
231
|
+
await expect(
|
|
232
|
+
git(root, ['-c', "alias.hold-pipes=!sh -c 'sleep 30 & exit 0'", 'hold-pipes'], undefined, 100),
|
|
233
|
+
).rejects.toThrow('timed out after 100ms');
|
|
234
|
+
});
|
|
235
|
+
}, 5_000);
|
|
236
|
+
|
|
229
237
|
it('repairs a scoped package from its project-name release tag without creating a package-name tag', async () => {
|
|
230
238
|
await withFixtureRepo(async (author) => {
|
|
231
239
|
await writeWorkspace(author);
|