@smoothbricks/cli 0.10.6 → 0.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -0
- package/dist/github-ci/index.d.ts +1 -0
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +23 -2
- package/dist/lib/json.d.ts +1 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +3 -18
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +1 -0
- package/dist/monorepo/managed-files.d.ts +6 -22
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +12 -47
- package/dist/monorepo/package-policy.js +2 -2
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +2 -0
- package/dist/monorepo/publish-workflow.js +1 -1
- package/dist/monorepo/runtime.d.ts +2 -2
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +11 -16
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +20 -23
- package/dist/nx/index.d.ts +7 -4
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +117 -56
- package/managed/raw/tooling/devenv +71 -0
- package/package.json +4 -4
- package/src/cli.ts +8 -1
- package/src/github-ci/index.test.ts +50 -1
- package/src/github-ci/index.ts +31 -2
- package/src/lib/json.ts +1 -1
- package/src/lib/workspace.ts +1 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +11 -3
- package/src/monorepo/managed-files.test.ts +31 -22
- package/src/monorepo/managed-files.ts +15 -68
- package/src/monorepo/package-policy.test.ts +31 -0
- package/src/monorepo/package-policy.ts +2 -2
- package/src/monorepo/packs/index.ts +2 -0
- package/src/monorepo/publish-workflow.ts +5 -0
- package/src/monorepo/runtime.test.ts +23 -17
- package/src/monorepo/runtime.ts +12 -17
- package/src/monorepo/tool-validation.test.ts +15 -8
- package/src/monorepo/tool-validation.ts +20 -25
- package/src/nx/index.test.ts +45 -14
- package/src/nx/index.ts +133 -64
- package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
|
@@ -4,17 +4,17 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { LINUX_PLATFORM_TARGET_GLOBS, PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
6
6
|
import fc from 'fast-check';
|
|
7
|
+
import { type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
7
8
|
import {
|
|
8
9
|
deployTargetInfoFromProjects,
|
|
9
10
|
extractInlineLocalBlocksForTest,
|
|
10
11
|
INLINE_LOCAL_BEGIN,
|
|
11
12
|
INLINE_LOCAL_END,
|
|
12
13
|
LOCAL_SECTION_MARKER,
|
|
13
|
-
|
|
14
|
+
managedFileTargetsForTest,
|
|
14
15
|
platformTargetGlobsForTest,
|
|
15
16
|
reinsertInlineLocalBlocksForTest,
|
|
16
17
|
splitLocalSectionForTest,
|
|
17
|
-
targetNamesFromProjects,
|
|
18
18
|
} from './managed-files.js';
|
|
19
19
|
|
|
20
20
|
const MANAGED = '# managed content\npath merge=driver\n';
|
|
@@ -176,48 +176,57 @@ describe('managed publish platform discovery', () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
describe('nx graph project helpers', () => {
|
|
179
|
-
const
|
|
179
|
+
const sampleProjects: NxProjects = {
|
|
180
180
|
lib: {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
lint: {},
|
|
186
|
-
},
|
|
181
|
+
targets: {
|
|
182
|
+
build: {},
|
|
183
|
+
'bundle-linux': {},
|
|
184
|
+
lint: {},
|
|
187
185
|
},
|
|
188
186
|
},
|
|
189
187
|
app: {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
production: { options: { command: 'wrangler deploy --env production' } },
|
|
197
|
-
},
|
|
188
|
+
targets: {
|
|
189
|
+
deploy: {
|
|
190
|
+
options: { command: 'echo no' },
|
|
191
|
+
configurations: {
|
|
192
|
+
staging: { command: 'wrangler deploy --env staging' },
|
|
193
|
+
production: { options: { command: 'wrangler deploy --env production' } },
|
|
198
194
|
},
|
|
199
|
-
'package-macos': {},
|
|
200
195
|
},
|
|
196
|
+
'package-macos': {},
|
|
201
197
|
},
|
|
202
198
|
},
|
|
203
199
|
};
|
|
204
200
|
|
|
205
201
|
it('collects target names from graph nodes without project names', () => {
|
|
206
|
-
expect(targetNamesFromProjects(
|
|
202
|
+
expect(targetNamesFromProjects(sampleProjects).sort()).toEqual(
|
|
207
203
|
['build', 'bundle-linux', 'deploy', 'lint', 'package-macos'].sort(),
|
|
208
204
|
);
|
|
209
205
|
});
|
|
210
206
|
|
|
211
207
|
it('detects deploy configurations and cloudflare provider from graph nodes', () => {
|
|
212
|
-
expect(deployTargetInfoFromProjects(
|
|
208
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'staging')).toEqual({
|
|
213
209
|
exists: true,
|
|
214
210
|
provider: 'cloudflare',
|
|
215
211
|
});
|
|
216
|
-
expect(deployTargetInfoFromProjects(
|
|
212
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'production')).toEqual({
|
|
217
213
|
exists: true,
|
|
218
214
|
provider: 'cloudflare',
|
|
219
215
|
});
|
|
220
|
-
expect(deployTargetInfoFromProjects(
|
|
216
|
+
expect(deployTargetInfoFromProjects(sampleProjects, 'preview')).toEqual({ exists: false });
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('managed raw files', () => {
|
|
221
|
+
it('manages the devenv wrapper as an executable byte-exact copy', async () => {
|
|
222
|
+
expect(managedFileTargetsForTest).toContainEqual({ target: 'tooling/devenv', executable: true });
|
|
223
|
+
|
|
224
|
+
const [source, generated] = await Promise.all([
|
|
225
|
+
readFile(join(REPO_ROOT, 'packages', 'cli', 'managed', 'raw', 'tooling', 'devenv'), 'utf8'),
|
|
226
|
+
readFile(join(REPO_ROOT, 'tooling', 'devenv'), 'utf8'),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
expect(generated).toBe(source);
|
|
221
230
|
});
|
|
222
231
|
});
|
|
223
232
|
|
|
@@ -2,10 +2,9 @@ import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFi
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
5
|
-
|
|
6
|
-
import { createProjectGraphAsync } from 'nx/src/project-graph/project-graph.js';
|
|
7
|
-
import type { PackageJson } from '../lib/json.js';
|
|
5
|
+
import type { NxTargetConfig, PackageJson } from '../lib/json.js';
|
|
8
6
|
import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
|
|
7
|
+
import { loadNxProjects, type NxProjects, targetNamesFromProjects } from '../nx/index.js';
|
|
9
8
|
import { renderCiWorkflowYaml } from './ci-workflow.js';
|
|
10
9
|
import { renderPublishWorkflowYaml } from './publish-workflow.js';
|
|
11
10
|
|
|
@@ -155,6 +154,12 @@ const managedFiles: ManagedFile[] = [
|
|
|
155
154
|
source: 'envrc',
|
|
156
155
|
target: '.envrc',
|
|
157
156
|
},
|
|
157
|
+
{
|
|
158
|
+
kind: 'raw',
|
|
159
|
+
source: 'tooling/devenv',
|
|
160
|
+
target: 'tooling/devenv',
|
|
161
|
+
executable: true,
|
|
162
|
+
},
|
|
158
163
|
{
|
|
159
164
|
kind: 'raw',
|
|
160
165
|
source: 'tooling/direnv/repo-path',
|
|
@@ -201,11 +206,6 @@ const managedFiles: ManagedFile[] = [
|
|
|
201
206
|
target: 'tooling/direnv/merge-newer-pins.sh',
|
|
202
207
|
executable: true,
|
|
203
208
|
},
|
|
204
|
-
{
|
|
205
|
-
kind: 'raw',
|
|
206
|
-
source: 'patches/ttsc@0.19.3.patch',
|
|
207
|
-
target: 'patches/ttsc@0.19.3.patch',
|
|
208
|
-
},
|
|
209
209
|
{
|
|
210
210
|
kind: 'generated',
|
|
211
211
|
source: 'ci-workflow',
|
|
@@ -254,6 +254,8 @@ const managedFiles: ManagedFile[] = [
|
|
|
254
254
|
},
|
|
255
255
|
];
|
|
256
256
|
|
|
257
|
+
export const managedFileTargetsForTest = managedFiles.map(({ target, executable }) => ({ target, executable }));
|
|
258
|
+
|
|
257
259
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
258
260
|
|
|
259
261
|
export async function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): Promise<FileResult[]> {
|
|
@@ -337,7 +339,7 @@ async function getManagedFileContext(root: string): Promise<ManagedFileContext>
|
|
|
337
339
|
const ciPushBranches = getCiPushBranches(packageJson?.json);
|
|
338
340
|
const ciRunsOn = getCiRunsOn(packageJson?.json);
|
|
339
341
|
// In-process Nx API → daemon socket (no second Node/`nx` CLI process).
|
|
340
|
-
const nxProjects = await
|
|
342
|
+
const nxProjects = await loadNxProjects(root);
|
|
341
343
|
const stagingDeploy = deployTargetInfoFromProjects(nxProjects, 'staging');
|
|
342
344
|
const productionDeploy = deployTargetInfoFromProjects(nxProjects, 'production');
|
|
343
345
|
const platformTargetGlobs = platformTargetGlobsForTest(targetNamesFromProjects(nxProjects));
|
|
@@ -366,67 +368,12 @@ export function platformTargetGlobsForTest(targetNames: Iterable<string>): strin
|
|
|
366
368
|
});
|
|
367
369
|
}
|
|
368
370
|
|
|
369
|
-
/** Resolved Nx project node as returned under `graph.nodes` / project-graph.json. */
|
|
370
|
-
export type NxGraphProjectNode = {
|
|
371
|
-
data?: { targets?: Record<string, NxGraphTarget> };
|
|
372
|
-
targets?: Record<string, NxGraphTarget>;
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
type NxGraphTarget = {
|
|
376
|
-
command?: string;
|
|
377
|
-
options?: { command?: string };
|
|
378
|
-
configurations?: Record<string, { command?: string; options?: { command?: string } }>;
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Load resolved project nodes via Nx's programmatic API.
|
|
383
|
-
*
|
|
384
|
-
* `createProjectGraphAsync` is what the CLI uses: with the daemon up it speaks the
|
|
385
|
-
* daemon unix socket instead of rebuilding the graph in a fresh Node process.
|
|
386
|
-
* Spawning `nx graph` / `nx show` only added CLI boot + plugin-worker overhead on
|
|
387
|
-
* top of that. smoo runs under Bun; Bun can import this CJS module directly.
|
|
388
|
-
*/
|
|
389
|
-
async function loadNxGraphProjects(root: string): Promise<Record<string, NxGraphProjectNode>> {
|
|
390
|
-
const prevCwd = process.cwd();
|
|
391
|
-
process.chdir(root);
|
|
392
|
-
try {
|
|
393
|
-
const graph = (await createProjectGraphAsync()) as {
|
|
394
|
-
nodes: Record<string, { data?: { targets?: Record<string, NxGraphTarget> } }>;
|
|
395
|
-
};
|
|
396
|
-
const out: Record<string, NxGraphProjectNode> = {};
|
|
397
|
-
for (const [name, node] of Object.entries(graph.nodes)) {
|
|
398
|
-
out[name] = { data: { targets: node.data?.targets ?? {} } };
|
|
399
|
-
}
|
|
400
|
-
return out;
|
|
401
|
-
} finally {
|
|
402
|
-
process.chdir(prevCwd);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function targetsOfProject(node: NxGraphProjectNode): Record<string, NxGraphTarget> {
|
|
407
|
-
return node.data?.targets ?? node.targets ?? {};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Test seam: collect target names from a graph.nodes map. */
|
|
411
|
-
export function targetNamesFromProjects(nodes: Record<string, NxGraphProjectNode>): string[] {
|
|
412
|
-
const targetNames = new Set<string>();
|
|
413
|
-
for (const node of Object.values(nodes)) {
|
|
414
|
-
for (const name of Object.keys(targetsOfProject(node))) {
|
|
415
|
-
targetNames.add(name);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return [...targetNames];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
371
|
/** Test seam: deploy configuration presence/provider from graph nodes. */
|
|
422
|
-
export function deployTargetInfoFromProjects(
|
|
423
|
-
nodes: Record<string, NxGraphProjectNode>,
|
|
424
|
-
configuration: string,
|
|
425
|
-
): DeployTargetInfo {
|
|
372
|
+
export function deployTargetInfoFromProjects(projects: NxProjects, configuration: string): DeployTargetInfo {
|
|
426
373
|
let exists = false;
|
|
427
374
|
let provider: DeployTargetInfo['provider'];
|
|
428
|
-
for (const
|
|
429
|
-
const info = deployTargetInfoFromTargets(
|
|
375
|
+
for (const project of Object.values(projects)) {
|
|
376
|
+
const info = deployTargetInfoFromTargets(project.targets ?? {}, configuration);
|
|
430
377
|
if (!info.exists) {
|
|
431
378
|
continue;
|
|
432
379
|
}
|
|
@@ -436,7 +383,7 @@ export function deployTargetInfoFromProjects(
|
|
|
436
383
|
return { exists, provider };
|
|
437
384
|
}
|
|
438
385
|
|
|
439
|
-
function deployTargetInfoFromTargets(targets: Record<string,
|
|
386
|
+
function deployTargetInfoFromTargets(targets: Record<string, NxTargetConfig>, configuration: string): DeployTargetInfo {
|
|
440
387
|
const deploy = targets.deploy;
|
|
441
388
|
if (!deploy) {
|
|
442
389
|
return { exists: false };
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
|
|
12
12
|
const TIMEOUT_FLAG = `--timeout=${BOUNDED_TEST_PER_TEST_TIMEOUT_MS}`;
|
|
13
13
|
|
|
14
|
+
import { validateCommitMessage } from './commit-msg.js';
|
|
15
|
+
|
|
14
16
|
import {
|
|
15
17
|
applyFixableMonorepoDefaults,
|
|
16
18
|
applyNxPluginDefaults,
|
|
@@ -245,6 +247,35 @@ describe('Nx project name policy', () => {
|
|
|
245
247
|
await rm(root, { recursive: true, force: true });
|
|
246
248
|
}
|
|
247
249
|
});
|
|
250
|
+
|
|
251
|
+
it('uses nx.name from an exact workspace package as the commit scope', async () => {
|
|
252
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-package-policy-'));
|
|
253
|
+
try {
|
|
254
|
+
await writeJson(join(root, 'package.json'), {
|
|
255
|
+
name: '@smoothbricks/codebase',
|
|
256
|
+
version: '0.0.0',
|
|
257
|
+
private: true,
|
|
258
|
+
workspaces: ['tooling'],
|
|
259
|
+
});
|
|
260
|
+
await writeJson(join(root, 'tooling/package.json'), {
|
|
261
|
+
name: '@smoothbricks/internal-tooling',
|
|
262
|
+
private: true,
|
|
263
|
+
nx: { name: 'tooling' },
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const validScopes = listValidCommitScopes(root);
|
|
267
|
+
expect(validScopes).toEqual(new Set(['release', 'tooling']));
|
|
268
|
+
expect(validateCommitMessage('fix(tooling): repair tooling\n', { validScopes })).toBeNull();
|
|
269
|
+
expect(validateCommitMessage('fix(internal-tooling): repair tooling\n', { validScopes })).toContain(
|
|
270
|
+
'Invalid conventional commit scope',
|
|
271
|
+
);
|
|
272
|
+
expect(validateCommitMessage('fix(arbitrary): repair tooling\n', { validScopes })).toContain(
|
|
273
|
+
'Invalid conventional commit scope',
|
|
274
|
+
);
|
|
275
|
+
} finally {
|
|
276
|
+
await rm(root, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
248
279
|
});
|
|
249
280
|
|
|
250
281
|
describe('workspace package script policy', () => {
|
|
@@ -165,7 +165,7 @@ export function applyWorkspaceDependencyDefaults(root: string, options: PackageT
|
|
|
165
165
|
} else {
|
|
166
166
|
console.log('unchanged tsconfig.test.json policy');
|
|
167
167
|
}
|
|
168
|
-
if (applyWorkspaceBoundedTestTargetPolicy(root)) {
|
|
168
|
+
if (applyWorkspaceBoundedTestTargetPolicy(root, options)) {
|
|
169
169
|
console.log('updated package test targets bounded execution policy');
|
|
170
170
|
} else {
|
|
171
171
|
console.log('unchanged package test targets bounded execution policy');
|
|
@@ -409,7 +409,7 @@ export function validateWorkspaceDependencies(root: string, options: PackageTarg
|
|
|
409
409
|
console.error(`${issue.path}: ${issue.message}`);
|
|
410
410
|
failures++;
|
|
411
411
|
}
|
|
412
|
-
for (const issue of checkWorkspaceBoundedTestTargetPolicy(root)) {
|
|
412
|
+
for (const issue of checkWorkspaceBoundedTestTargetPolicy(root, options)) {
|
|
413
413
|
console.error(`${issue.path}: ${issue.message}`);
|
|
414
414
|
failures++;
|
|
415
415
|
}
|
|
@@ -414,10 +414,12 @@ export function resolvedTargetsByProject(projects: ProjectTargets[]): Map<string
|
|
|
414
414
|
projects.map((project) => [
|
|
415
415
|
project.project,
|
|
416
416
|
{
|
|
417
|
+
...(project.root ? { root: project.root } : {}),
|
|
417
418
|
targets: new Set(project.targets),
|
|
418
419
|
...(project.buildDependsOn ? { buildDependsOn: project.buildDependsOn } : {}),
|
|
419
420
|
...(project.targetDependencies ? { targetDependencies: project.targetDependencies } : {}),
|
|
420
421
|
...(project.targetExecutors ? { targetExecutors: project.targetExecutors } : {}),
|
|
422
|
+
...(project.targetOptions ? { targetOptions: project.targetOptions } : {}),
|
|
421
423
|
...(project.targetScripts ? { targetScripts: project.targetScripts } : {}),
|
|
422
424
|
},
|
|
423
425
|
]),
|
|
@@ -785,6 +785,11 @@ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): st
|
|
|
785
785
|
` "${githubExpression('runner.temp')}/macos-platform-outputs"`,
|
|
786
786
|
'',
|
|
787
787
|
` # Step ${stepNumber++}`,
|
|
788
|
+
' - name: 🧪 Unit test macOS and iOS packages',
|
|
789
|
+
' run:',
|
|
790
|
+
` smoo github-ci nx-run-many --targets test --projects-with-targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}"`,
|
|
791
|
+
'',
|
|
792
|
+
` # Step ${stepNumber++}`,
|
|
788
793
|
' - name: 📤 Upload macOS platform outputs',
|
|
789
794
|
' uses: actions/upload-artifact@v7.0.1',
|
|
790
795
|
' with:',
|
|
@@ -3,16 +3,16 @@ import type { PackageJson } from '../lib/json.js';
|
|
|
3
3
|
import { runtimeTypesRangeForPublishedVersions, validateRuntimePins } from './runtime.js';
|
|
4
4
|
|
|
5
5
|
describe('runtimeTypesRangeForPublishedVersions', () => {
|
|
6
|
-
test('
|
|
6
|
+
test('anchors a caret range at the newest published version in the installed Node major', () => {
|
|
7
7
|
expect(
|
|
8
8
|
runtimeTypesRangeForPublishedVersions('@types/node', '24.12.0', 'major', ['24.0.0', '24.12.4', '25.9.1']),
|
|
9
|
-
).toBe('
|
|
9
|
+
).toBe('^24.12.4');
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
test('
|
|
13
|
-
expect(
|
|
12
|
+
test('rejects fallback types from a different Node major', () => {
|
|
13
|
+
expect(() =>
|
|
14
14
|
runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['24.12.4', '25.9.0', '25.9.1']),
|
|
15
|
-
).
|
|
15
|
+
).toThrow('runtime major 26');
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
test('uses the installed Bun version when @types/bun has published it', () => {
|
|
@@ -23,10 +23,10 @@ describe('runtimeTypesRangeForPublishedVersions', () => {
|
|
|
23
23
|
expect(runtimeTypesRangeForPublishedVersions('@types/bun', '1.3.15', 'exact', ['1.3.13', '1.3.14'])).toBe('1.3.14');
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
test('ignores non-stable version strings when choosing the
|
|
27
|
-
expect(
|
|
28
|
-
'
|
|
29
|
-
);
|
|
26
|
+
test('ignores non-stable version strings when choosing the same-major anchor', () => {
|
|
27
|
+
expect(
|
|
28
|
+
runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['25.9.1', '26.0.2', '26.1.0-beta.1']),
|
|
29
|
+
).toBe('^26.0.2');
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -35,23 +35,29 @@ describe('validateRuntimePins', () => {
|
|
|
35
35
|
const aligned = () => ({
|
|
36
36
|
engines: { node: '>=24.0.0' },
|
|
37
37
|
packageManager: 'bun@1.3.14',
|
|
38
|
-
devDependencies: { '@types/node': '
|
|
38
|
+
devDependencies: { '@types/node': '^24.13.0' },
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('
|
|
42
|
-
|
|
41
|
+
test('accepts caret ranges anchored at any patch or minor in the PATH Node major', () => {
|
|
42
|
+
for (const version of ['^24.0.3', '^24.13.0']) {
|
|
43
|
+
const pkg = aligned();
|
|
44
|
+
pkg.devDependencies['@types/node'] = version;
|
|
45
|
+
expect(validateRuntimePins(pkg, runtime)).toBe(0);
|
|
46
|
+
}
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
test('fails when @types/node tracks a different major than the PATH node', () => {
|
|
46
50
|
const pkg = aligned();
|
|
47
|
-
pkg.devDependencies['@types/node'] = '
|
|
51
|
+
pkg.devDependencies['@types/node'] = '^26.1.1';
|
|
48
52
|
expect(validateRuntimePins(pkg, runtime)).toBe(1);
|
|
49
53
|
});
|
|
50
54
|
|
|
51
|
-
test('
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
test('rejects exact and tilde @types/node pins even when the major matches', () => {
|
|
56
|
+
for (const version of ['24.13.0', '~24.13.0']) {
|
|
57
|
+
const pkg = aligned();
|
|
58
|
+
pkg.devDependencies['@types/node'] = version;
|
|
59
|
+
expect(validateRuntimePins(pkg, runtime)).toBe(1);
|
|
60
|
+
}
|
|
55
61
|
});
|
|
56
62
|
|
|
57
63
|
test('fails engines.node and packageManager drift against the PATH runtimes', () => {
|
package/src/monorepo/runtime.ts
CHANGED
|
@@ -66,8 +66,8 @@ export async function syncRootRuntimeVersions(root: string): Promise<void> {
|
|
|
66
66
|
/**
|
|
67
67
|
* Validate that package.json runtime pins agree with the LIVE PATH runtimes,
|
|
68
68
|
* never a stored template. Offline by design: structural alignment only
|
|
69
|
-
* (
|
|
70
|
-
* consulted. Repair: `smoo monorepo init --runtime-only` inside the devenv shell.
|
|
69
|
+
* (Node types float within the runtime major; Bun types stay exact) — the registry
|
|
70
|
+
* is not consulted. Repair: `smoo monorepo init --runtime-only` inside the devenv shell.
|
|
71
71
|
*/
|
|
72
72
|
export async function validateRootRuntimeVersions(root: string): Promise<number> {
|
|
73
73
|
const packageJson = readJsonObject(join(root, 'package.json'));
|
|
@@ -100,15 +100,10 @@ export function validateRuntimePins(packageJson: PackageJson, runtime: RuntimeVe
|
|
|
100
100
|
failures++;
|
|
101
101
|
}
|
|
102
102
|
const typesNode = packageJson.devDependencies?.['@types/node'] ?? null;
|
|
103
|
-
const typesNodeParts = typesNode ?
|
|
103
|
+
const typesNodeParts = typesNode ? /^\^(\d+)\.(\d+)\.(\d+)$/.exec(typesNode) : null;
|
|
104
104
|
if (!typesNodeParts || typesNodeParts[1] !== nodeMajor) {
|
|
105
105
|
console.error(
|
|
106
|
-
`package.json @types/node is ${typesNode ?? 'missing'} but the PATH node is v${runtime.node} —
|
|
107
|
-
);
|
|
108
|
-
failures++;
|
|
109
|
-
} else if (typesNodeParts[2] === '0' && typesNodeParts[3] === '0') {
|
|
110
|
-
console.error(
|
|
111
|
-
`package.json @types/node is pinned to the ~${nodeMajor}.0.0 floor — tilde locks the first patch line and strands the repo on early broken releases; ${repair} to repin to the newest published ${nodeMajor}.x`,
|
|
106
|
+
`package.json @types/node is ${typesNode ?? 'missing'} but the PATH node is v${runtime.node} — expected a caret range within the runtime major (^${nodeMajor}.x.y); ${repair}`,
|
|
112
107
|
);
|
|
113
108
|
failures++;
|
|
114
109
|
}
|
|
@@ -144,15 +139,15 @@ export function runtimeTypesRangeForPublishedVersions(
|
|
|
144
139
|
|
|
145
140
|
if (pinMode === 'major') {
|
|
146
141
|
const runtimeMajor = parsedRuntimeVersion[0].toString();
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// broken early release (e.g. @types/node 24.0.x's URLPattern/DOM TS2403
|
|
150
|
-
// conflict, fixed in 24.13) with no path to the repaired minors.
|
|
142
|
+
// Anchor the range at the newest published types in the runtime major. The caret
|
|
143
|
+
// admits later patches and minors without crossing into a different Node major.
|
|
151
144
|
const latestInMajor = latestVersion(versions.filter((version) => versionMajor(version) === runtimeMajor));
|
|
152
|
-
if (latestInMajor) {
|
|
153
|
-
|
|
145
|
+
if (!latestInMajor) {
|
|
146
|
+
throw new Error(`Unable to find published ${packageName} versions for runtime major ${runtimeMajor}`);
|
|
154
147
|
}
|
|
155
|
-
|
|
148
|
+
return `^${latestInMajor}`;
|
|
149
|
+
}
|
|
150
|
+
if (versions.includes(runtimeVersion)) {
|
|
156
151
|
return runtimeVersion;
|
|
157
152
|
}
|
|
158
153
|
|
|
@@ -162,7 +157,7 @@ export function runtimeTypesRangeForPublishedVersions(
|
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
console.warn(`${packageName} has no published types for runtime ${runtimeVersion}; using ${latest}`);
|
|
165
|
-
return
|
|
160
|
+
return latest;
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
function latestVersion(versions: readonly string[]): string | null {
|
|
@@ -19,7 +19,7 @@ const registryVersions: Record<string, string[]> = {
|
|
|
19
19
|
'eslint-stdout': ['1.1.1', '1.1.2'],
|
|
20
20
|
nx: ['23.1.0'],
|
|
21
21
|
prettier: ['3.6.1'],
|
|
22
|
-
ttsc: ['0.
|
|
22
|
+
ttsc: ['0.25.0'],
|
|
23
23
|
typescript: ['6.0.3'],
|
|
24
24
|
// alias package is not fetched from registry under this name when using fallback
|
|
25
25
|
'@typescript/native': ['7.0.2'],
|
|
@@ -50,6 +50,9 @@ describe('tool configuration validation', () => {
|
|
|
50
50
|
devDependencies: {
|
|
51
51
|
'@smoothbricks/cli': 'workspace:*',
|
|
52
52
|
},
|
|
53
|
+
patchedDependencies: {
|
|
54
|
+
'ttsc@0.19.3': 'patches/ttsc@0.19.3.patch',
|
|
55
|
+
},
|
|
53
56
|
});
|
|
54
57
|
const devenvPath = join(root, 'tooling/direnv/devenv.nix');
|
|
55
58
|
await mkdir(dirname(devenvPath), { recursive: true });
|
|
@@ -76,10 +79,10 @@ describe('tool configuration validation', () => {
|
|
|
76
79
|
expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe('workspace:*');
|
|
77
80
|
expect(rootPackage.devDependencies['eslint-stdout']).toBe('workspace:*');
|
|
78
81
|
expect(rootPackage.devDependencies.nx).toBe('23.1.0');
|
|
79
|
-
expect(rootPackage.devDependencies.ttsc).toBe('0.
|
|
82
|
+
expect(rootPackage.devDependencies.ttsc).toBe('0.25.0');
|
|
80
83
|
expect(rootPackage.devDependencies.typescript).toBe('^6.0.3');
|
|
81
84
|
expect(rootPackage.devDependencies['@typescript/native']).toBe('npm:typescript@^7.0.2');
|
|
82
|
-
expect(rootPackage.patchedDependencies['ttsc@0.19.3']).
|
|
85
|
+
expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
|
|
83
86
|
expect(rootPackage.workspaces).toContain('tooling');
|
|
84
87
|
expect(toolingPackage.name).toBe('@smoothbricks/tooling');
|
|
85
88
|
expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
|
|
@@ -91,7 +94,7 @@ describe('tool configuration validation', () => {
|
|
|
91
94
|
}
|
|
92
95
|
});
|
|
93
96
|
|
|
94
|
-
it('accepts newer
|
|
97
|
+
it('accepts newer dependencies after removing even an empty obsolete ttsc patch key', async () => {
|
|
95
98
|
const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
|
|
96
99
|
try {
|
|
97
100
|
await writeJson(join(root, 'package.json'), {
|
|
@@ -108,12 +111,12 @@ describe('tool configuration validation', () => {
|
|
|
108
111
|
'eslint-stdout': await currentEslintStdoutRange(),
|
|
109
112
|
nx: '24.0.0',
|
|
110
113
|
prettier: '^3.7.0',
|
|
111
|
-
ttsc: '0.
|
|
114
|
+
ttsc: '0.25.0',
|
|
112
115
|
typescript: '^6.0.0',
|
|
113
116
|
'@typescript/native': 'npm:typescript@^7.0.2',
|
|
114
117
|
},
|
|
115
118
|
patchedDependencies: {
|
|
116
|
-
'ttsc@0.19.3': '
|
|
119
|
+
'ttsc@0.19.3': '',
|
|
117
120
|
},
|
|
118
121
|
});
|
|
119
122
|
await writeJson(join(root, 'tooling/package.json'), {
|
|
@@ -143,7 +146,11 @@ describe('tool configuration validation', () => {
|
|
|
143
146
|
`,
|
|
144
147
|
);
|
|
145
148
|
|
|
149
|
+
expect(await validateToolConfig(root)).toBe(1);
|
|
150
|
+
await applyToolConfigDefaults(root);
|
|
146
151
|
expect(await validateToolConfig(root)).toBe(0);
|
|
152
|
+
const rootPackage = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
153
|
+
expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
|
|
147
154
|
} finally {
|
|
148
155
|
await rm(root, { recursive: true, force: true });
|
|
149
156
|
}
|
|
@@ -210,7 +217,7 @@ describe('tool configuration validation', () => {
|
|
|
210
217
|
eslint: '^10.0.0',
|
|
211
218
|
nx: '24.0.0',
|
|
212
219
|
prettier: '^3.7.0',
|
|
213
|
-
ttsc: '0.
|
|
220
|
+
ttsc: '0.25.0',
|
|
214
221
|
typescript: '^6.0.0',
|
|
215
222
|
},
|
|
216
223
|
});
|
|
@@ -224,7 +231,7 @@ describe('tool configuration validation', () => {
|
|
|
224
231
|
expect(rootPackage.devDependencies['eslint-stdout']).toBe(await currentEslintStdoutRange());
|
|
225
232
|
expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe(await currentNxPluginRange());
|
|
226
233
|
expect(rootPackage.devDependencies['@typescript/native']).toBe('npm:typescript@^7.0.2');
|
|
227
|
-
expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).
|
|
234
|
+
expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
|
|
228
235
|
expect(toolingPackage.name).toBe('@fixture/tooling');
|
|
229
236
|
expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe(await currentCliRange());
|
|
230
237
|
} finally {
|
|
@@ -56,8 +56,7 @@ const rootDevDependencies: RequiredDependency[] = [
|
|
|
56
56
|
},
|
|
57
57
|
{ name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
|
|
58
58
|
{ name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', prefix: '^' },
|
|
59
|
-
|
|
60
|
-
{ name: 'ttsc', fallbackVersion: '0.19.3' },
|
|
59
|
+
{ name: 'ttsc', fallbackVersion: '0.25.0' },
|
|
61
60
|
// Nx and typescript-eslint still load the TypeScript JS API (6.x).
|
|
62
61
|
// Compilation is exclusively delegated to ttsc by the Nx plugin targets.
|
|
63
62
|
{ name: 'typescript', fallbackVersion: '^6.0.3', minimumVersion: '6.0.0', prefix: '^' },
|
|
@@ -75,8 +74,7 @@ const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejan
|
|
|
75
74
|
// never against a version template here.
|
|
76
75
|
const nodePackagePattern = /(^|\s)nodejs(_\d+|_latest)?(\s|#|$)/m;
|
|
77
76
|
|
|
78
|
-
const
|
|
79
|
-
const TTSC_PATCH_PATH = 'patches/ttsc@0.19.3.patch';
|
|
77
|
+
const obsoleteTtscPatchedDependencyKey = 'ttsc@0.19.3';
|
|
80
78
|
|
|
81
79
|
export async function applyToolConfigDefaults(root: string): Promise<void> {
|
|
82
80
|
const context = await readToolContext(root);
|
|
@@ -89,7 +87,7 @@ export async function validateToolConfig(root: string): Promise<number> {
|
|
|
89
87
|
const context = await readToolContext(root);
|
|
90
88
|
return (
|
|
91
89
|
validateRootDevDependencies(context.policy, context.rootPackage) +
|
|
92
|
-
|
|
90
|
+
validateObsoleteTtscPatchRemoved(context.rootPackage) +
|
|
93
91
|
validateToolingPackage(root, context.policy) +
|
|
94
92
|
validateToolingWorkspace(context.rootPackage) +
|
|
95
93
|
validateDevenvPackages(root)
|
|
@@ -113,7 +111,7 @@ export async function applyRootDevDependencyDefaults(root: string, context: Tool
|
|
|
113
111
|
if (delete devDependencies['@smoothbricks/cli']) {
|
|
114
112
|
changed = true;
|
|
115
113
|
}
|
|
116
|
-
changed =
|
|
114
|
+
changed = removeObsoleteTtscPatch(pkg) || changed;
|
|
117
115
|
if (changed) {
|
|
118
116
|
writeJsonObject(join(root, 'package.json'), pkg);
|
|
119
117
|
console.log('updated package.json workspace tool dependencies');
|
|
@@ -129,7 +127,6 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
|
|
|
129
127
|
}
|
|
130
128
|
let dependencyChanged = false;
|
|
131
129
|
let workspaceChanged = false;
|
|
132
|
-
let patchChanged = false;
|
|
133
130
|
const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
|
|
134
131
|
for (const dependency of rootDevDependencies) {
|
|
135
132
|
const current = devDependencies[dependency.name];
|
|
@@ -142,8 +139,8 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
|
|
|
142
139
|
dependencyChanged = true;
|
|
143
140
|
}
|
|
144
141
|
workspaceChanged = addWorkspacePattern(pkg, 'tooling');
|
|
145
|
-
|
|
146
|
-
if (dependencyChanged || workspaceChanged
|
|
142
|
+
dependencyChanged = removeObsoleteTtscPatch(pkg) || dependencyChanged;
|
|
143
|
+
if (dependencyChanged || workspaceChanged) {
|
|
147
144
|
writeJsonObject(join(root, 'package.json'), pkg);
|
|
148
145
|
}
|
|
149
146
|
console.log(
|
|
@@ -156,11 +153,6 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
|
|
|
156
153
|
? 'updated package.json tooling workspace'
|
|
157
154
|
: 'unchanged package.json tooling workspace',
|
|
158
155
|
);
|
|
159
|
-
console.log(
|
|
160
|
-
patchChanged
|
|
161
|
-
? 'updated package.json patchedDependencies for ttsc'
|
|
162
|
-
: 'unchanged package.json patchedDependencies for ttsc',
|
|
163
|
-
);
|
|
164
156
|
}
|
|
165
157
|
|
|
166
158
|
export function applyToolingPackageDefaults(root: string, policy: ToolPolicy): void {
|
|
@@ -252,22 +244,25 @@ export function validateRootDevDependencies(policy: ToolPolicy, rootPackage: Pac
|
|
|
252
244
|
return failures;
|
|
253
245
|
}
|
|
254
246
|
|
|
255
|
-
function
|
|
256
|
-
const
|
|
257
|
-
|
|
247
|
+
function removeObsoleteTtscPatch(pkg: PackageJson): boolean {
|
|
248
|
+
const patchedDependencies = pkg.patchedDependencies;
|
|
249
|
+
if (!patchedDependencies || !(obsoleteTtscPatchedDependencyKey in patchedDependencies)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
delete patchedDependencies[obsoleteTtscPatchedDependencyKey];
|
|
253
|
+
if (Object.keys(patchedDependencies).length === 0) {
|
|
254
|
+
delete pkg.patchedDependencies;
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
258
257
|
}
|
|
259
258
|
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
const actual = rootPackage.patchedDependencies?.[TTSC_PATCHED_DEPENDENCY_KEY];
|
|
265
|
-
if (actual === TTSC_PATCH_PATH) {
|
|
259
|
+
function validateObsoleteTtscPatchRemoved(rootPackage: PackageJson | null): number {
|
|
260
|
+
const patchedDependencies = rootPackage?.patchedDependencies;
|
|
261
|
+
if (!patchedDependencies || !(obsoleteTtscPatchedDependencyKey in patchedDependencies)) {
|
|
266
262
|
return 0;
|
|
267
263
|
}
|
|
268
264
|
console.error(
|
|
269
|
-
`package.json patchedDependencies.${
|
|
270
|
-
(typeof actual === 'string' ? `; found ${actual}` : ' (required for declaration-emit fix)'),
|
|
265
|
+
`package.json patchedDependencies.${obsoleteTtscPatchedDependencyKey} must be removed; ttsc 0.25.0 emits typia declarations without the obsolete patch`,
|
|
271
266
|
);
|
|
272
267
|
return 1;
|
|
273
268
|
}
|