@smoothbricks/cli 0.10.3 → 0.10.5
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 +9 -13
- package/dist/bun/preload.d.ts +13 -0
- package/dist/bun/preload.d.ts.map +1 -0
- package/dist/bun/preload.js +12 -0
- package/dist/cli.js +13 -5
- package/dist/generate/index.d.ts.map +1 -1
- package/dist/generate/index.js +0 -6
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +52 -16
- package/dist/github-ci/outputs.d.ts.map +1 -1
- package/dist/github-ci/outputs.js +1 -2
- package/dist/lib/cli-package.js +2 -2
- package/dist/lib/json.d.ts +120 -11
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +319 -29
- package/dist/lib/workspace.d.ts +7 -14
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +76 -82
- package/dist/monorepo/ci-workflow.d.ts +2 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +27 -1
- package/dist/monorepo/git-config.js +2 -2
- package/dist/monorepo/index.d.ts +4 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +19 -3
- package/dist/monorepo/lockfile.d.ts +14 -1
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +33 -21
- package/dist/monorepo/managed-files.d.ts +11 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +57 -32
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +52 -45
- package/dist/monorepo/packed-manifest.d.ts +5 -2
- package/dist/monorepo/packed-manifest.d.ts.map +1 -1
- package/dist/monorepo/packed-manifest.js +16 -47
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +28 -16
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +14 -5
- package/dist/monorepo/publish-workflow.js +3 -1
- package/dist/monorepo/runtime.d.ts +15 -0
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +66 -12
- package/dist/monorepo/tool-validation.d.ts +2 -2
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +141 -38
- package/dist/monorepo/wrangler.d.ts.map +1 -1
- package/dist/monorepo/wrangler.js +13 -13
- package/dist/nx/index.d.ts +9 -7
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +48 -31
- package/dist/pr/index.d.ts.map +1 -1
- package/dist/pr/index.js +28 -32
- package/dist/release/candidates.d.ts +3 -2
- package/dist/release/candidates.d.ts.map +1 -1
- package/dist/release/candidates.js +4 -5
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +88 -59
- package/dist/wrangler/prepare-env.d.ts +0 -1
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +100 -51
- package/dist/wrangler/scaffold.d.ts.map +1 -1
- package/dist/wrangler/scaffold.js +12 -12
- package/managed/raw/envrc +5 -3
- package/managed/raw/git-format-staged.yml +19 -1
- package/managed/raw/patches/ttsc@0.19.3.patch +67 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +2 -1
- package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
- package/managed/raw/tooling/direnv/setup-environment.ts +170 -19
- package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
- package/managed/templates/github/actions/cache-node-modules/action.yml +5 -2
- package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
- package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
- package/managed/templates/github/workflows/managed-files.yml +80 -0
- package/package.json +15 -4
- package/src/bun/preload.ts +12 -0
- package/src/cli.ts +20 -8
- package/src/generate/index.ts +0 -6
- package/src/github-ci/index.ts +49 -18
- package/src/github-ci/outputs.ts +1 -2
- package/src/lib/cli-package.ts +2 -2
- package/src/lib/json.ts +293 -32
- package/src/lib/workspace.ts +88 -100
- package/src/monorepo/__tests__/ci-workflow.test.ts +3 -3
- package/src/monorepo/ci-workflow.ts +27 -1
- package/src/monorepo/git-config.ts +2 -2
- package/src/monorepo/index.ts +19 -3
- package/src/monorepo/lockfile.test.ts +35 -36
- package/src/monorepo/lockfile.ts +49 -24
- package/src/monorepo/managed-files.test.ts +1 -0
- package/src/monorepo/managed-files.ts +62 -38
- package/src/monorepo/package-policy.test.ts +20 -50
- package/src/monorepo/package-policy.ts +76 -59
- package/src/monorepo/packed-manifest.ts +18 -52
- package/src/monorepo/packed-package.ts +43 -22
- package/src/monorepo/packs/index.ts +14 -5
- package/src/monorepo/publish-workflow.ts +3 -1
- package/src/monorepo/runtime.test.ts +42 -3
- package/src/monorepo/runtime.ts +90 -12
- package/src/monorepo/tool-validation.test.ts +38 -16
- package/src/monorepo/tool-validation.ts +149 -41
- package/src/monorepo/wrangler.test.ts +11 -15
- package/src/monorepo/wrangler.ts +15 -15
- package/src/nx/index.test.ts +9 -5
- package/src/nx/index.ts +69 -42
- package/src/pr/index.ts +42 -36
- package/src/release/__tests__/candidates.test.ts +5 -4
- package/src/release/__tests__/trust-publisher.test.ts +7 -0
- package/src/release/candidates.ts +10 -17
- package/src/release/index.ts +110 -73
- package/src/wrangler/prepare-env.ts +75 -54
- package/src/wrangler/scaffold.test.ts +9 -12
- package/src/wrangler/scaffold.ts +13 -13
package/src/release/index.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1
2
|
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
4
5
|
import { createInterface } from 'node:readline/promises';
|
|
5
6
|
import { Writable } from 'node:stream';
|
|
6
7
|
import { $ } from 'bun';
|
|
8
|
+
import typia from 'typia';
|
|
7
9
|
import { githubCiApplyOutputs, githubCiNxRunMany } from '../github-ci/index.js';
|
|
8
10
|
import { assertNoConflictMarkers } from '../lib/conflict-markers.js';
|
|
9
11
|
import { withDevenvEnv } from '../lib/devenv.js';
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
type NxJson,
|
|
14
|
+
type NxProjectJson,
|
|
15
|
+
type NxTargetConfig,
|
|
16
|
+
type PackageJson,
|
|
17
|
+
parseNxJsonText,
|
|
18
|
+
parseNxProjectJsonText,
|
|
19
|
+
parsePackageJsonText,
|
|
20
|
+
} from '../lib/json.js';
|
|
11
21
|
import { decode, run, runInteractiveStatus, runResult } from '../lib/run.js';
|
|
12
22
|
import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
|
|
23
|
+
import { syncBunLockfileVersions } from '../monorepo/lockfile.js';
|
|
13
24
|
import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
|
|
14
25
|
import {
|
|
15
26
|
type BootstrapNpmPackagesOptions,
|
|
@@ -519,6 +530,10 @@ async function listUnpublishedPackages(root: string, packages: ReleasePackage[])
|
|
|
519
530
|
async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: string, dryRun: boolean): Promise<void> {
|
|
520
531
|
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
|
|
521
532
|
const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
|
|
533
|
+
// bun pm pack resolves workspace:* from bun.lock. Day-to-day lock keeps -next
|
|
534
|
+
// (install/CI). For publish, temporarily rewrite unpublished -next entries to
|
|
535
|
+
// the last stable tag so the tarball embeds installable versions, then restore.
|
|
536
|
+
syncBunLockfileVersions(root, { mode: 'publish', log: true });
|
|
522
537
|
try {
|
|
523
538
|
console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
|
|
524
539
|
await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
|
|
@@ -550,6 +565,7 @@ async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: stri
|
|
|
550
565
|
},
|
|
551
566
|
);
|
|
552
567
|
} finally {
|
|
568
|
+
syncBunLockfileVersions(root, { mode: 'install', log: true });
|
|
553
569
|
await rm(tempDir, { recursive: true, force: true });
|
|
554
570
|
}
|
|
555
571
|
}
|
|
@@ -572,7 +588,7 @@ async function publishPlaceholderPackage(
|
|
|
572
588
|
name: pkg.name,
|
|
573
589
|
version: NPM_BOOTSTRAP_VERSION,
|
|
574
590
|
description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
|
|
575
|
-
license:
|
|
591
|
+
license: pkg.json.license ?? 'UNLICENSED',
|
|
576
592
|
repository: pkg.json.repository,
|
|
577
593
|
publishConfig: { access: 'public' },
|
|
578
594
|
},
|
|
@@ -596,7 +612,7 @@ function safeTarballPrefix(name: string): string {
|
|
|
596
612
|
|
|
597
613
|
async function assertPackedWorkspaceDependencies(root: string, tarball: string, pkg: ReleasePackage): Promise<void> {
|
|
598
614
|
const manifest = await readPackedPackageJson(root, tarball, pkg.name);
|
|
599
|
-
const failures = validatePackedWorkspaceDependencies(root, pkg, manifest);
|
|
615
|
+
const failures = validatePackedWorkspaceDependencies(root, pkg, manifest, { mode: 'publish' });
|
|
600
616
|
if (failures.length > 0) {
|
|
601
617
|
throw new Error(failures.join('\n'));
|
|
602
618
|
}
|
|
@@ -783,8 +799,7 @@ async function packageVersionAtRef(root: string, packagePath: string, ref: strin
|
|
|
783
799
|
return null;
|
|
784
800
|
}
|
|
785
801
|
try {
|
|
786
|
-
|
|
787
|
-
return isRecord(parsed) ? stringProperty(parsed, 'version') : null;
|
|
802
|
+
return parsePackageJsonText(decode(result.stdout))?.version ?? null;
|
|
788
803
|
} catch {
|
|
789
804
|
return null;
|
|
790
805
|
}
|
|
@@ -1264,74 +1279,83 @@ async function packageChangedFilesSince(root: string, ref: string, packagePath:
|
|
|
1264
1279
|
.map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
|
|
1265
1280
|
}
|
|
1266
1281
|
|
|
1267
|
-
async function packageJsonAtRef(
|
|
1268
|
-
root: string,
|
|
1269
|
-
ref: string,
|
|
1270
|
-
packagePath: string,
|
|
1271
|
-
): Promise<Record<string, unknown> | null> {
|
|
1282
|
+
async function packageJsonAtRef(root: string, ref: string, packagePath: string): Promise<PackageJson | null> {
|
|
1272
1283
|
const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
1273
1284
|
if (result.exitCode !== 0) {
|
|
1274
1285
|
return null;
|
|
1275
1286
|
}
|
|
1276
1287
|
try {
|
|
1277
|
-
|
|
1278
|
-
return isRecord(parsed) ? parsed : null;
|
|
1288
|
+
return parsePackageJsonText(decode(result.stdout));
|
|
1279
1289
|
} catch {
|
|
1280
1290
|
return null;
|
|
1281
1291
|
}
|
|
1282
1292
|
}
|
|
1283
1293
|
|
|
1284
|
-
async function currentPackageJson(root: string, packagePath: string): Promise<
|
|
1294
|
+
async function currentPackageJson(root: string, packagePath: string): Promise<PackageJson | null> {
|
|
1285
1295
|
return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
|
|
1286
1296
|
}
|
|
1287
1297
|
|
|
1288
1298
|
async function packageBuildInputPatterns(root: string, projectName: string, _packagePath: string): Promise<string[]> {
|
|
1289
1299
|
const project = await nxProjectJson(root, projectName);
|
|
1290
|
-
|
|
1291
|
-
return resolveBuildInputPatterns(project, nxJson);
|
|
1300
|
+
return resolveBuildInputPatterns(project, readNxJson(join(root, 'nx.json')));
|
|
1292
1301
|
}
|
|
1293
1302
|
|
|
1294
|
-
async function nxProjectJson(root: string, projectName: string): Promise<
|
|
1303
|
+
async function nxProjectJson(root: string, projectName: string): Promise<NxProjectJson> {
|
|
1295
1304
|
const result = await $`nx show project ${projectName} --json`.cwd(root).quiet();
|
|
1296
|
-
const parsed =
|
|
1297
|
-
if (!
|
|
1305
|
+
const parsed = parseNxProjectJsonText(decode(result.stdout));
|
|
1306
|
+
if (!parsed) {
|
|
1298
1307
|
throw new Error(`Unable to inspect Nx project ${projectName}.`);
|
|
1299
1308
|
}
|
|
1300
1309
|
return parsed;
|
|
1301
1310
|
}
|
|
1302
1311
|
|
|
1303
|
-
function
|
|
1304
|
-
|
|
1312
|
+
function readNxJson(path: string): NxJson {
|
|
1313
|
+
if (!existsSync(path)) {
|
|
1314
|
+
return {};
|
|
1315
|
+
}
|
|
1316
|
+
try {
|
|
1317
|
+
return parseNxJsonText(readFileSync(path, 'utf8')) ?? {};
|
|
1318
|
+
} catch {
|
|
1319
|
+
return {};
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function resolveBuildInputPatterns(project: NxProjectJson, nxJson: NxJson): string[] {
|
|
1324
|
+
const targets = project.targets;
|
|
1305
1325
|
if (!targets) {
|
|
1306
1326
|
return [];
|
|
1307
1327
|
}
|
|
1308
1328
|
return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
|
|
1309
1329
|
}
|
|
1310
1330
|
|
|
1311
|
-
function collectBuildInputs(targets: Record<string,
|
|
1312
|
-
const build =
|
|
1331
|
+
function collectBuildInputs(targets: Record<string, NxTargetConfig>): string[] {
|
|
1332
|
+
const build = targets.build;
|
|
1313
1333
|
if (!build) {
|
|
1314
1334
|
return ['production'];
|
|
1315
1335
|
}
|
|
1316
|
-
const directInputs =
|
|
1336
|
+
const directInputs = stringInputs(build.inputs);
|
|
1317
1337
|
if (directInputs.length > 0) {
|
|
1318
1338
|
return directInputs;
|
|
1319
1339
|
}
|
|
1320
1340
|
const inputs: string[] = [];
|
|
1321
|
-
for (const dependency of
|
|
1322
|
-
if (dependency.startsWith('^')) {
|
|
1341
|
+
for (const dependency of build.dependsOn ?? []) {
|
|
1342
|
+
if (typeof dependency !== 'string' || dependency.startsWith('^')) {
|
|
1323
1343
|
continue;
|
|
1324
1344
|
}
|
|
1325
1345
|
const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
|
|
1326
1346
|
if (!targetName) {
|
|
1327
1347
|
continue;
|
|
1328
1348
|
}
|
|
1329
|
-
inputs.push(...
|
|
1349
|
+
inputs.push(...stringInputs(targets[targetName]?.inputs));
|
|
1330
1350
|
}
|
|
1331
1351
|
return inputs.length > 0 ? inputs : ['production'];
|
|
1332
1352
|
}
|
|
1333
1353
|
|
|
1334
|
-
function
|
|
1354
|
+
function stringInputs(inputs: NxTargetConfig['inputs'] | undefined): string[] {
|
|
1355
|
+
return Array.isArray(inputs) ? inputs.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function normalizeInputPatterns(inputs: string[], nxJson: NxJson): string[] {
|
|
1335
1359
|
const patterns: string[] = [];
|
|
1336
1360
|
const seen = new Set<string>();
|
|
1337
1361
|
for (const input of inputs) {
|
|
@@ -1342,14 +1366,13 @@ function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown
|
|
|
1342
1366
|
return patterns;
|
|
1343
1367
|
}
|
|
1344
1368
|
|
|
1345
|
-
function expandInputPattern(input: string, nxJson:
|
|
1369
|
+
function expandInputPattern(input: string, nxJson: NxJson, seen: Set<string>): string[] {
|
|
1346
1370
|
if (seen.has(input)) {
|
|
1347
1371
|
return [];
|
|
1348
1372
|
}
|
|
1349
1373
|
seen.add(input);
|
|
1350
1374
|
if (!input.includes('{')) {
|
|
1351
|
-
const
|
|
1352
|
-
const namedInput = namedInputs?.[input];
|
|
1375
|
+
const namedInput = nxJson.namedInputs?.[input];
|
|
1353
1376
|
if (Array.isArray(namedInput)) {
|
|
1354
1377
|
return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
|
|
1355
1378
|
}
|
|
@@ -1364,19 +1387,6 @@ function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen
|
|
|
1364
1387
|
return [`${excluded ? '!' : ''}${packageRelative}`];
|
|
1365
1388
|
}
|
|
1366
1389
|
|
|
1367
|
-
function recordProperty(record: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
|
|
1368
|
-
if (!record) {
|
|
1369
|
-
return null;
|
|
1370
|
-
}
|
|
1371
|
-
const value = record[key];
|
|
1372
|
-
return isRecord(value) ? value : null;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
function stringArrayProperty(record: Record<string, unknown> | null, key: string): string[] {
|
|
1376
|
-
const value = record?.[key];
|
|
1377
|
-
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
1390
|
async function packageHasHistory(root: string, packagePath: string): Promise<boolean> {
|
|
1381
1391
|
const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
1382
1392
|
if (result.exitCode !== 0) {
|
|
@@ -1615,7 +1625,7 @@ async function runLatestNpmTrust(
|
|
|
1615
1625
|
if (status === 0) {
|
|
1616
1626
|
return 'configured';
|
|
1617
1627
|
}
|
|
1618
|
-
throw new Error(
|
|
1628
|
+
throw new Error(npmCommandFailedMessage(npmArgs, status));
|
|
1619
1629
|
}
|
|
1620
1630
|
|
|
1621
1631
|
async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisherLookup> {
|
|
@@ -1624,7 +1634,7 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
|
|
|
1624
1634
|
if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
|
|
1625
1635
|
const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1626
1636
|
if (status !== 0) {
|
|
1627
|
-
throw new Error(
|
|
1637
|
+
throw new Error(npmCommandFailedMessage(args, status));
|
|
1628
1638
|
}
|
|
1629
1639
|
result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1630
1640
|
}
|
|
@@ -1632,15 +1642,47 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
|
|
|
1632
1642
|
return npmTrustAccessDetails(root, pkg.name);
|
|
1633
1643
|
}
|
|
1634
1644
|
if (result.exitCode !== 0) {
|
|
1635
|
-
throw new Error(
|
|
1636
|
-
`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`,
|
|
1637
|
-
);
|
|
1645
|
+
throw new Error(npmCommandFailedMessage(args, result.exitCode, result.stdout, result.stderr));
|
|
1638
1646
|
}
|
|
1639
1647
|
return parseTrustedPublishers(result.stdout, pkg.name);
|
|
1640
1648
|
}
|
|
1641
1649
|
|
|
1650
|
+
/** npm trust list/setup requires an authenticated package owner. */
|
|
1642
1651
|
export function npmTrustListAccessDenied(stdout: string, stderr: string): boolean {
|
|
1643
|
-
|
|
1652
|
+
const output = `${stdout}\n${stderr}`;
|
|
1653
|
+
// E403: authenticated but not an owner. E401 / login-required: no usable session.
|
|
1654
|
+
return (
|
|
1655
|
+
/\bE403\b/.test(output) ||
|
|
1656
|
+
/\bE401\b/.test(output) ||
|
|
1657
|
+
/You must be logged in to publish packages/i.test(output) ||
|
|
1658
|
+
/npm error 401 Unauthorized/i.test(output)
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function npmCommandFailedMessage(npmArgs: string[], exitCode: number, stdout = '', stderr = ''): string {
|
|
1663
|
+
const command = `nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')}`;
|
|
1664
|
+
const detail = npmFailureDetail(stdout, stderr);
|
|
1665
|
+
return detail.length > 0
|
|
1666
|
+
? `${command} failed with exit code ${exitCode}\n${detail}`
|
|
1667
|
+
: `${command} failed with exit code ${exitCode}`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function npmFailureDetail(stdout: string, stderr: string): string {
|
|
1671
|
+
const chunks = [stderr.trim(), stdout.trim()].filter((chunk) => chunk.length > 0);
|
|
1672
|
+
if (chunks.length === 0) {
|
|
1673
|
+
return '';
|
|
1674
|
+
}
|
|
1675
|
+
// Prefer the actionable npm error line over notice spam when present.
|
|
1676
|
+
const joined = chunks.join('\n');
|
|
1677
|
+
const errorLines = joined
|
|
1678
|
+
.split('\n')
|
|
1679
|
+
.map((line) => line.trimEnd())
|
|
1680
|
+
.filter((line) => /npm error|error code|401 Unauthorized|403 Forbidden|E401|E403|logged in/i.test(line));
|
|
1681
|
+
if (errorLines.length > 0) {
|
|
1682
|
+
return errorLines.slice(0, 8).join('\n');
|
|
1683
|
+
}
|
|
1684
|
+
// Fall back to a short tail of combined output.
|
|
1685
|
+
return joined.split('\n').slice(-12).join('\n');
|
|
1644
1686
|
}
|
|
1645
1687
|
|
|
1646
1688
|
async function npmTrustAccessDetails(root: string, packageName: string): Promise<NpmTrustAccessDenied> {
|
|
@@ -1665,36 +1707,31 @@ export function parseTrustedPublishers(stdout: string, packageName: string): Tru
|
|
|
1665
1707
|
if (stdout.trim().length === 0) {
|
|
1666
1708
|
return [];
|
|
1667
1709
|
}
|
|
1668
|
-
|
|
1710
|
+
// createIsParse returns null for non-matching shapes and bare `null`.
|
|
1711
|
+
const parsed = parseTrustedPublisherJson(stdout);
|
|
1669
1712
|
if (parsed === null) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
}
|
|
1675
|
-
if (isRecord(parsed)) {
|
|
1676
|
-
return [parseTrustedPublisher(parsed, packageName)];
|
|
1713
|
+
if (stdout.trim() === 'null') {
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
throw new Error(`${packageName}: npm trust list returned invalid trusted publisher JSON.`);
|
|
1677
1717
|
}
|
|
1678
|
-
|
|
1718
|
+
return (Array.isArray(parsed) ? parsed : [parsed]).map((value) => ({
|
|
1719
|
+
id: value.id,
|
|
1720
|
+
type: value.type,
|
|
1721
|
+
file: value.file,
|
|
1722
|
+
repository: value.repository,
|
|
1723
|
+
}));
|
|
1679
1724
|
}
|
|
1680
1725
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const type = stringProperty(value, 'type');
|
|
1687
|
-
if (!id || !type) {
|
|
1688
|
-
throw new Error(`${packageName}: npm trust list returned a trusted publisher without id or type.`);
|
|
1689
|
-
}
|
|
1690
|
-
return {
|
|
1691
|
-
id,
|
|
1692
|
-
type,
|
|
1693
|
-
file: stringProperty(value, 'file') ?? undefined,
|
|
1694
|
-
repository: stringProperty(value, 'repository') ?? undefined,
|
|
1695
|
-
};
|
|
1726
|
+
interface TrustedPublisherJson {
|
|
1727
|
+
id: string;
|
|
1728
|
+
type: string;
|
|
1729
|
+
file?: string;
|
|
1730
|
+
repository?: string;
|
|
1696
1731
|
}
|
|
1697
1732
|
|
|
1733
|
+
const parseTrustedPublisherJson = typia.json.createIsParse<TrustedPublisherJson | TrustedPublisherJson[]>();
|
|
1734
|
+
|
|
1698
1735
|
async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
|
|
1699
1736
|
await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
|
|
1700
1737
|
}
|
|
@@ -20,13 +20,50 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { $ } from 'bun';
|
|
22
22
|
import { type AST, getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
|
|
23
|
+
import typia from 'typia';
|
|
23
24
|
|
|
24
|
-
// ── runtime narrowing (wrangler JSON is external data — no casts)
|
|
25
|
+
// ── runtime narrowing (wrangler TOML/JSON is external data — no casts) ───────
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
interface WranglerTomlRoot {
|
|
28
|
+
env?: Record<string, WranglerEnvBlock | undefined>;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
interface WranglerEnvBlock {
|
|
32
|
+
vars?: Record<string, unknown>;
|
|
33
|
+
kv_namespaces?: WranglerKvRow[];
|
|
34
|
+
d1_databases?: WranglerD1Row[];
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface WranglerKvRow {
|
|
39
|
+
binding?: string;
|
|
40
|
+
id?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface WranglerD1Row {
|
|
44
|
+
binding?: string;
|
|
45
|
+
database_id?: string;
|
|
46
|
+
database_name?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface WranglerSecretRow {
|
|
50
|
+
name?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ShellErrorShape {
|
|
54
|
+
stderr?: unknown;
|
|
55
|
+
exitCode?: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isWranglerTomlRoot = typia.createIs<WranglerTomlRoot>();
|
|
59
|
+
const isWranglerEnvBlock = typia.createIs<WranglerEnvBlock>();
|
|
60
|
+
const isWranglerKvRow = typia.createIs<WranglerKvRow>();
|
|
61
|
+
const isWranglerD1Row = typia.createIs<WranglerD1Row>();
|
|
62
|
+
const isWranglerSecretRow = typia.createIs<WranglerSecretRow>();
|
|
63
|
+
const isKvNamespaceRow = typia.createIs<{ id: string; title: string }>();
|
|
64
|
+
const isD1DatabaseRow = typia.createIs<{ uuid: string; name: string }>();
|
|
65
|
+
const isShellErrorShape = typia.createIs<ShellErrorShape>();
|
|
66
|
+
|
|
30
67
|
// ── wrangler.toml editors (CST: read via getStaticTOMLValue, write via
|
|
31
68
|
// node-range splice — comment/format-preserving by construction, no regex) ─────
|
|
32
69
|
|
|
@@ -36,15 +73,14 @@ function tables(toml: string): AST.TOMLTable[] {
|
|
|
36
73
|
}
|
|
37
74
|
|
|
38
75
|
/** Whole doc as a JS object (comments/format dropped — reads only). */
|
|
39
|
-
function tomlValue(toml: string):
|
|
40
|
-
const value = getStaticTOMLValue(parseTOML(toml));
|
|
41
|
-
return
|
|
76
|
+
function tomlValue(toml: string): WranglerTomlRoot {
|
|
77
|
+
const value: unknown = getStaticTOMLValue(parseTOML(toml));
|
|
78
|
+
return isWranglerTomlRoot(value) ? value : {};
|
|
42
79
|
}
|
|
43
80
|
|
|
44
|
-
function envRecord(toml: string, env: string):
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
return isRecord(block) ? block : null;
|
|
81
|
+
function envRecord(toml: string, env: string): WranglerEnvBlock | null {
|
|
82
|
+
const block = tomlValue(toml).env?.[env];
|
|
83
|
+
return isWranglerEnvBlock(block) ? block : null;
|
|
48
84
|
}
|
|
49
85
|
|
|
50
86
|
function sameKey(a: (string | number)[], b: (string | number)[]): boolean {
|
|
@@ -68,8 +104,13 @@ function arrayTableKv(
|
|
|
68
104
|
field: string,
|
|
69
105
|
): AST.TOMLKeyValue | null {
|
|
70
106
|
const block = envRecord(toml, env);
|
|
71
|
-
const rows =
|
|
72
|
-
const index = rows.findIndex((
|
|
107
|
+
const rows = Array.isArray(block?.[arrayKey]) ? block[arrayKey] : [];
|
|
108
|
+
const index = rows.findIndex((row) => {
|
|
109
|
+
if (arrayKey === 'd1_databases') {
|
|
110
|
+
return isWranglerD1Row(row) && row.binding === binding;
|
|
111
|
+
}
|
|
112
|
+
return isWranglerKvRow(row) && row.binding === binding;
|
|
113
|
+
});
|
|
73
114
|
if (index < 0) return null;
|
|
74
115
|
const table = tables(toml).find((t) => t.kind === 'array' && sameKey(t.resolvedKey, ['env', env, arrayKey, index]));
|
|
75
116
|
return table ? kvIn(toml, table, field) : null;
|
|
@@ -88,7 +129,7 @@ function endOfLine(toml: string, offset: number): number {
|
|
|
88
129
|
/** First `[env.<name>]` (source order; also derived from a nested `[env.<name>.*]`), or null when no env declared. */
|
|
89
130
|
export function firstWranglerEnv(toml: string): string | null {
|
|
90
131
|
const envs = tomlValue(toml).env;
|
|
91
|
-
if (!
|
|
132
|
+
if (!envs) return null;
|
|
92
133
|
const [first] = Object.keys(envs);
|
|
93
134
|
return first ?? null;
|
|
94
135
|
}
|
|
@@ -96,14 +137,12 @@ export function firstWranglerEnv(toml: string): string | null {
|
|
|
96
137
|
/** True if the toml declares `[env.<env>]` (or any `[env.<env>.*]` sub-table). */
|
|
97
138
|
export function hasEnvBlock(toml: string, env: string): boolean {
|
|
98
139
|
const envs = tomlValue(toml).env;
|
|
99
|
-
return
|
|
140
|
+
return !!envs && env in envs;
|
|
100
141
|
}
|
|
101
142
|
|
|
102
143
|
/** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
|
|
103
144
|
export function getVar(toml: string, env: string, name: string): string | null {
|
|
104
|
-
const
|
|
105
|
-
const vars = isRecord(block?.vars) ? block.vars : {};
|
|
106
|
-
const value = vars[name];
|
|
145
|
+
const value = envRecord(toml, env)?.vars?.[name];
|
|
107
146
|
return typeof value === 'string' && value ? value : null;
|
|
108
147
|
}
|
|
109
148
|
|
|
@@ -117,10 +156,8 @@ export function setVar(toml: string, env: string, name: string, value: string):
|
|
|
117
156
|
|
|
118
157
|
/** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
|
|
119
158
|
export function getKvId(toml: string, env: string, binding: string): string | null {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
for (const row of rows) {
|
|
123
|
-
if (isRecord(row) && row.binding === binding && typeof row.id === 'string') return row.id || null;
|
|
159
|
+
for (const row of envRecord(toml, env)?.kv_namespaces ?? []) {
|
|
160
|
+
if (row.binding === binding && typeof row.id === 'string') return row.id || null;
|
|
124
161
|
}
|
|
125
162
|
return null;
|
|
126
163
|
}
|
|
@@ -166,21 +203,16 @@ export function blankKvIds(toml: string, env: string): string {
|
|
|
166
203
|
|
|
167
204
|
/** The database_id written for `[[env.<env>.d1_databases]]` binding, or null if absent/empty. */
|
|
168
205
|
export function getD1Id(toml: string, env: string, binding: string): string | null {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
for (const row of rows) {
|
|
172
|
-
if (isRecord(row) && row.binding === binding && typeof row.database_id === 'string') return row.database_id || null;
|
|
206
|
+
for (const row of envRecord(toml, env)?.d1_databases ?? []) {
|
|
207
|
+
if (row.binding === binding && typeof row.database_id === 'string') return row.database_id || null;
|
|
173
208
|
}
|
|
174
209
|
return null;
|
|
175
210
|
}
|
|
176
211
|
|
|
177
212
|
/** The `database_name` for `[[env.<env>.d1_databases]]` binding, or null. Source of truth for which DB to create/reuse. */
|
|
178
213
|
export function getD1Name(toml: string, env: string, binding: string): string | null {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
for (const row of rows) {
|
|
182
|
-
if (isRecord(row) && row.binding === binding && typeof row.database_name === 'string')
|
|
183
|
-
return row.database_name || null;
|
|
214
|
+
for (const row of envRecord(toml, env)?.d1_databases ?? []) {
|
|
215
|
+
if (row.binding === binding && typeof row.database_name === 'string') return row.database_name || null;
|
|
184
216
|
}
|
|
185
217
|
return null;
|
|
186
218
|
}
|
|
@@ -260,12 +292,13 @@ export function parseDevVarsExample(text: string): string[] {
|
|
|
260
292
|
export function readManifest(root: string, env: string): Manifest {
|
|
261
293
|
const toml = readFileSync(join(root, 'wrangler.toml'), 'utf8');
|
|
262
294
|
const block = envRecord(toml, env);
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
295
|
+
const vars = Object.keys(block?.vars ?? {});
|
|
296
|
+
const kvBindings = (block?.kv_namespaces ?? []).flatMap((row) =>
|
|
297
|
+
typeof row.binding === 'string' ? [row.binding] : [],
|
|
298
|
+
);
|
|
299
|
+
const d1Bindings = (block?.d1_databases ?? []).flatMap((row) =>
|
|
300
|
+
typeof row.binding === 'string' ? [row.binding] : [],
|
|
301
|
+
);
|
|
269
302
|
const examplePath = join(root, '.dev.vars.example');
|
|
270
303
|
const secrets = existsSync(examplePath) ? parseDevVarsExample(readFileSync(examplePath, 'utf8')) : [];
|
|
271
304
|
return { kvBindings, d1Bindings, vars, secrets };
|
|
@@ -300,25 +333,19 @@ export interface KvNamespace {
|
|
|
300
333
|
|
|
301
334
|
function asKvNamespaces(json: unknown): KvNamespace[] {
|
|
302
335
|
if (!Array.isArray(json)) return [];
|
|
303
|
-
|
|
304
|
-
for (const row of json) {
|
|
305
|
-
if (isRecord(row) && typeof row.id === 'string' && typeof row.title === 'string') {
|
|
306
|
-
out.push({ id: row.id, title: row.title });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return out;
|
|
336
|
+
return json.filter(isKvNamespaceRow);
|
|
310
337
|
}
|
|
311
338
|
|
|
312
339
|
function asSecretNames(json: unknown): string[] {
|
|
313
340
|
if (!Array.isArray(json)) return [];
|
|
314
|
-
return json.flatMap((row) => (
|
|
341
|
+
return json.flatMap((row) => (isWranglerSecretRow(row) && typeof row.name === 'string' ? [row.name] : []));
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
/** Best-effort ERROR line from a Bun ShellError (thrown on non-zero exit). */
|
|
318
345
|
export function errText(err: unknown): string {
|
|
319
|
-
if (!
|
|
320
|
-
const stderr =
|
|
321
|
-
const exit =
|
|
346
|
+
if (!isShellErrorShape(err)) return 'unknown error';
|
|
347
|
+
const stderr = err.stderr != null ? String(err.stderr) : '';
|
|
348
|
+
const exit = err.exitCode != null ? String(err.exitCode) : '?';
|
|
322
349
|
return stderr.split('\n').find((l) => l.includes('ERROR')) ?? `exit ${exit}`;
|
|
323
350
|
}
|
|
324
351
|
|
|
@@ -358,13 +385,7 @@ export interface D1Database {
|
|
|
358
385
|
|
|
359
386
|
function asD1Databases(json: unknown): D1Database[] {
|
|
360
387
|
if (!Array.isArray(json)) return [];
|
|
361
|
-
|
|
362
|
-
for (const row of json) {
|
|
363
|
-
if (isRecord(row) && typeof row.uuid === 'string' && typeof row.name === 'string') {
|
|
364
|
-
out.push({ uuid: row.uuid, name: row.name });
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return out;
|
|
388
|
+
return json.filter(isD1DatabaseRow);
|
|
368
389
|
}
|
|
369
390
|
|
|
370
391
|
/** Live D1 databases on the account (`d1 list --json`); `[]` when offline/unauthenticated. */
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it, spyOn } from 'bun:test';
|
|
|
2
2
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { type PackageJson, parsePackageJsonText } from '../lib/json.js';
|
|
5
6
|
import { scaffold } from './scaffold.js';
|
|
6
7
|
|
|
7
8
|
describe('scaffold', () => {
|
|
@@ -164,27 +165,23 @@ async function createWorkspace(
|
|
|
164
165
|
return root;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
function nxTargets(pkg:
|
|
168
|
-
const
|
|
169
|
-
if (!
|
|
168
|
+
function nxTargets(pkg: PackageJson): NonNullable<NonNullable<PackageJson['nx']>['targets']> {
|
|
169
|
+
const targets = pkg.nx?.targets;
|
|
170
|
+
if (!targets) {
|
|
170
171
|
throw new Error('nx.targets not found');
|
|
171
172
|
}
|
|
172
|
-
return
|
|
173
|
+
return targets;
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
async function readJson(path: string): Promise<Record<string, unknown>> {
|
|
180
|
-
const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
|
|
181
|
-
if (!isRecord(parsed)) {
|
|
176
|
+
async function readJson(path: string): Promise<PackageJson> {
|
|
177
|
+
const parsed = parsePackageJsonText(await readFile(path, 'utf8'));
|
|
178
|
+
if (!parsed) {
|
|
182
179
|
throw new Error('expected JSON object');
|
|
183
180
|
}
|
|
184
181
|
return parsed;
|
|
185
182
|
}
|
|
186
183
|
|
|
187
|
-
async function writeJson(path: string, value:
|
|
184
|
+
async function writeJson(path: string, value: PackageJson): Promise<void> {
|
|
188
185
|
await mkdir(join(path, '..'), { recursive: true });
|
|
189
186
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
190
187
|
}
|