@smoothbricks/cli 0.10.3 → 0.10.4
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 +12 -4
- 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 +118 -11
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +317 -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 +3 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +6 -2
- package/dist/monorepo/lockfile.d.ts +13 -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 +52 -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 +13 -4
- 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.map +1 -1
- package/dist/monorepo/tool-validation.js +32 -23
- 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 +82 -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/git-format-staged.yml +19 -1
- 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 +150 -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 +19 -7
- 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 +291 -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 +6 -2
- package/src/monorepo/lockfile.test.ts +35 -36
- package/src/monorepo/lockfile.ts +48 -24
- package/src/monorepo/managed-files.test.ts +1 -0
- package/src/monorepo/managed-files.ts +57 -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 +13 -4
- 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.ts +35 -25
- 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 +104 -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,13 +1,23 @@
|
|
|
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';
|
|
13
23
|
import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
|
|
@@ -572,7 +582,7 @@ async function publishPlaceholderPackage(
|
|
|
572
582
|
name: pkg.name,
|
|
573
583
|
version: NPM_BOOTSTRAP_VERSION,
|
|
574
584
|
description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
|
|
575
|
-
license:
|
|
585
|
+
license: pkg.json.license ?? 'UNLICENSED',
|
|
576
586
|
repository: pkg.json.repository,
|
|
577
587
|
publishConfig: { access: 'public' },
|
|
578
588
|
},
|
|
@@ -596,7 +606,7 @@ function safeTarballPrefix(name: string): string {
|
|
|
596
606
|
|
|
597
607
|
async function assertPackedWorkspaceDependencies(root: string, tarball: string, pkg: ReleasePackage): Promise<void> {
|
|
598
608
|
const manifest = await readPackedPackageJson(root, tarball, pkg.name);
|
|
599
|
-
const failures = validatePackedWorkspaceDependencies(root, pkg, manifest);
|
|
609
|
+
const failures = validatePackedWorkspaceDependencies(root, pkg, manifest, { mode: 'publish' });
|
|
600
610
|
if (failures.length > 0) {
|
|
601
611
|
throw new Error(failures.join('\n'));
|
|
602
612
|
}
|
|
@@ -783,8 +793,7 @@ async function packageVersionAtRef(root: string, packagePath: string, ref: strin
|
|
|
783
793
|
return null;
|
|
784
794
|
}
|
|
785
795
|
try {
|
|
786
|
-
|
|
787
|
-
return isRecord(parsed) ? stringProperty(parsed, 'version') : null;
|
|
796
|
+
return parsePackageJsonText(decode(result.stdout))?.version ?? null;
|
|
788
797
|
} catch {
|
|
789
798
|
return null;
|
|
790
799
|
}
|
|
@@ -1264,74 +1273,83 @@ async function packageChangedFilesSince(root: string, ref: string, packagePath:
|
|
|
1264
1273
|
.map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
|
|
1265
1274
|
}
|
|
1266
1275
|
|
|
1267
|
-
async function packageJsonAtRef(
|
|
1268
|
-
root: string,
|
|
1269
|
-
ref: string,
|
|
1270
|
-
packagePath: string,
|
|
1271
|
-
): Promise<Record<string, unknown> | null> {
|
|
1276
|
+
async function packageJsonAtRef(root: string, ref: string, packagePath: string): Promise<PackageJson | null> {
|
|
1272
1277
|
const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
1273
1278
|
if (result.exitCode !== 0) {
|
|
1274
1279
|
return null;
|
|
1275
1280
|
}
|
|
1276
1281
|
try {
|
|
1277
|
-
|
|
1278
|
-
return isRecord(parsed) ? parsed : null;
|
|
1282
|
+
return parsePackageJsonText(decode(result.stdout));
|
|
1279
1283
|
} catch {
|
|
1280
1284
|
return null;
|
|
1281
1285
|
}
|
|
1282
1286
|
}
|
|
1283
1287
|
|
|
1284
|
-
async function currentPackageJson(root: string, packagePath: string): Promise<
|
|
1288
|
+
async function currentPackageJson(root: string, packagePath: string): Promise<PackageJson | null> {
|
|
1285
1289
|
return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
|
|
1286
1290
|
}
|
|
1287
1291
|
|
|
1288
1292
|
async function packageBuildInputPatterns(root: string, projectName: string, _packagePath: string): Promise<string[]> {
|
|
1289
1293
|
const project = await nxProjectJson(root, projectName);
|
|
1290
|
-
|
|
1291
|
-
return resolveBuildInputPatterns(project, nxJson);
|
|
1294
|
+
return resolveBuildInputPatterns(project, readNxJson(join(root, 'nx.json')));
|
|
1292
1295
|
}
|
|
1293
1296
|
|
|
1294
|
-
async function nxProjectJson(root: string, projectName: string): Promise<
|
|
1297
|
+
async function nxProjectJson(root: string, projectName: string): Promise<NxProjectJson> {
|
|
1295
1298
|
const result = await $`nx show project ${projectName} --json`.cwd(root).quiet();
|
|
1296
|
-
const parsed =
|
|
1297
|
-
if (!
|
|
1299
|
+
const parsed = parseNxProjectJsonText(decode(result.stdout));
|
|
1300
|
+
if (!parsed) {
|
|
1298
1301
|
throw new Error(`Unable to inspect Nx project ${projectName}.`);
|
|
1299
1302
|
}
|
|
1300
1303
|
return parsed;
|
|
1301
1304
|
}
|
|
1302
1305
|
|
|
1303
|
-
function
|
|
1304
|
-
|
|
1306
|
+
function readNxJson(path: string): NxJson {
|
|
1307
|
+
if (!existsSync(path)) {
|
|
1308
|
+
return {};
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
return parseNxJsonText(readFileSync(path, 'utf8')) ?? {};
|
|
1312
|
+
} catch {
|
|
1313
|
+
return {};
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function resolveBuildInputPatterns(project: NxProjectJson, nxJson: NxJson): string[] {
|
|
1318
|
+
const targets = project.targets;
|
|
1305
1319
|
if (!targets) {
|
|
1306
1320
|
return [];
|
|
1307
1321
|
}
|
|
1308
1322
|
return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
|
|
1309
1323
|
}
|
|
1310
1324
|
|
|
1311
|
-
function collectBuildInputs(targets: Record<string,
|
|
1312
|
-
const build =
|
|
1325
|
+
function collectBuildInputs(targets: Record<string, NxTargetConfig>): string[] {
|
|
1326
|
+
const build = targets.build;
|
|
1313
1327
|
if (!build) {
|
|
1314
1328
|
return ['production'];
|
|
1315
1329
|
}
|
|
1316
|
-
const directInputs =
|
|
1330
|
+
const directInputs = stringInputs(build.inputs);
|
|
1317
1331
|
if (directInputs.length > 0) {
|
|
1318
1332
|
return directInputs;
|
|
1319
1333
|
}
|
|
1320
1334
|
const inputs: string[] = [];
|
|
1321
|
-
for (const dependency of
|
|
1322
|
-
if (dependency.startsWith('^')) {
|
|
1335
|
+
for (const dependency of build.dependsOn ?? []) {
|
|
1336
|
+
if (typeof dependency !== 'string' || dependency.startsWith('^')) {
|
|
1323
1337
|
continue;
|
|
1324
1338
|
}
|
|
1325
1339
|
const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
|
|
1326
1340
|
if (!targetName) {
|
|
1327
1341
|
continue;
|
|
1328
1342
|
}
|
|
1329
|
-
inputs.push(...
|
|
1343
|
+
inputs.push(...stringInputs(targets[targetName]?.inputs));
|
|
1330
1344
|
}
|
|
1331
1345
|
return inputs.length > 0 ? inputs : ['production'];
|
|
1332
1346
|
}
|
|
1333
1347
|
|
|
1334
|
-
function
|
|
1348
|
+
function stringInputs(inputs: NxTargetConfig['inputs'] | undefined): string[] {
|
|
1349
|
+
return Array.isArray(inputs) ? inputs.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function normalizeInputPatterns(inputs: string[], nxJson: NxJson): string[] {
|
|
1335
1353
|
const patterns: string[] = [];
|
|
1336
1354
|
const seen = new Set<string>();
|
|
1337
1355
|
for (const input of inputs) {
|
|
@@ -1342,14 +1360,13 @@ function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown
|
|
|
1342
1360
|
return patterns;
|
|
1343
1361
|
}
|
|
1344
1362
|
|
|
1345
|
-
function expandInputPattern(input: string, nxJson:
|
|
1363
|
+
function expandInputPattern(input: string, nxJson: NxJson, seen: Set<string>): string[] {
|
|
1346
1364
|
if (seen.has(input)) {
|
|
1347
1365
|
return [];
|
|
1348
1366
|
}
|
|
1349
1367
|
seen.add(input);
|
|
1350
1368
|
if (!input.includes('{')) {
|
|
1351
|
-
const
|
|
1352
|
-
const namedInput = namedInputs?.[input];
|
|
1369
|
+
const namedInput = nxJson.namedInputs?.[input];
|
|
1353
1370
|
if (Array.isArray(namedInput)) {
|
|
1354
1371
|
return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
|
|
1355
1372
|
}
|
|
@@ -1364,19 +1381,6 @@ function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen
|
|
|
1364
1381
|
return [`${excluded ? '!' : ''}${packageRelative}`];
|
|
1365
1382
|
}
|
|
1366
1383
|
|
|
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
1384
|
async function packageHasHistory(root: string, packagePath: string): Promise<boolean> {
|
|
1381
1385
|
const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
1382
1386
|
if (result.exitCode !== 0) {
|
|
@@ -1615,7 +1619,7 @@ async function runLatestNpmTrust(
|
|
|
1615
1619
|
if (status === 0) {
|
|
1616
1620
|
return 'configured';
|
|
1617
1621
|
}
|
|
1618
|
-
throw new Error(
|
|
1622
|
+
throw new Error(npmCommandFailedMessage(npmArgs, status));
|
|
1619
1623
|
}
|
|
1620
1624
|
|
|
1621
1625
|
async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisherLookup> {
|
|
@@ -1624,7 +1628,7 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
|
|
|
1624
1628
|
if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
|
|
1625
1629
|
const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1626
1630
|
if (status !== 0) {
|
|
1627
|
-
throw new Error(
|
|
1631
|
+
throw new Error(npmCommandFailedMessage(args, status));
|
|
1628
1632
|
}
|
|
1629
1633
|
result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1630
1634
|
}
|
|
@@ -1632,15 +1636,47 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
|
|
|
1632
1636
|
return npmTrustAccessDetails(root, pkg.name);
|
|
1633
1637
|
}
|
|
1634
1638
|
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
|
-
);
|
|
1639
|
+
throw new Error(npmCommandFailedMessage(args, result.exitCode, result.stdout, result.stderr));
|
|
1638
1640
|
}
|
|
1639
1641
|
return parseTrustedPublishers(result.stdout, pkg.name);
|
|
1640
1642
|
}
|
|
1641
1643
|
|
|
1644
|
+
/** npm trust list/setup requires an authenticated package owner. */
|
|
1642
1645
|
export function npmTrustListAccessDenied(stdout: string, stderr: string): boolean {
|
|
1643
|
-
|
|
1646
|
+
const output = `${stdout}\n${stderr}`;
|
|
1647
|
+
// E403: authenticated but not an owner. E401 / login-required: no usable session.
|
|
1648
|
+
return (
|
|
1649
|
+
/\bE403\b/.test(output) ||
|
|
1650
|
+
/\bE401\b/.test(output) ||
|
|
1651
|
+
/You must be logged in to publish packages/i.test(output) ||
|
|
1652
|
+
/npm error 401 Unauthorized/i.test(output)
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function npmCommandFailedMessage(npmArgs: string[], exitCode: number, stdout = '', stderr = ''): string {
|
|
1657
|
+
const command = `nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')}`;
|
|
1658
|
+
const detail = npmFailureDetail(stdout, stderr);
|
|
1659
|
+
return detail.length > 0
|
|
1660
|
+
? `${command} failed with exit code ${exitCode}\n${detail}`
|
|
1661
|
+
: `${command} failed with exit code ${exitCode}`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function npmFailureDetail(stdout: string, stderr: string): string {
|
|
1665
|
+
const chunks = [stderr.trim(), stdout.trim()].filter((chunk) => chunk.length > 0);
|
|
1666
|
+
if (chunks.length === 0) {
|
|
1667
|
+
return '';
|
|
1668
|
+
}
|
|
1669
|
+
// Prefer the actionable npm error line over notice spam when present.
|
|
1670
|
+
const joined = chunks.join('\n');
|
|
1671
|
+
const errorLines = joined
|
|
1672
|
+
.split('\n')
|
|
1673
|
+
.map((line) => line.trimEnd())
|
|
1674
|
+
.filter((line) => /npm error|error code|401 Unauthorized|403 Forbidden|E401|E403|logged in/i.test(line));
|
|
1675
|
+
if (errorLines.length > 0) {
|
|
1676
|
+
return errorLines.slice(0, 8).join('\n');
|
|
1677
|
+
}
|
|
1678
|
+
// Fall back to a short tail of combined output.
|
|
1679
|
+
return joined.split('\n').slice(-12).join('\n');
|
|
1644
1680
|
}
|
|
1645
1681
|
|
|
1646
1682
|
async function npmTrustAccessDetails(root: string, packageName: string): Promise<NpmTrustAccessDenied> {
|
|
@@ -1665,36 +1701,31 @@ export function parseTrustedPublishers(stdout: string, packageName: string): Tru
|
|
|
1665
1701
|
if (stdout.trim().length === 0) {
|
|
1666
1702
|
return [];
|
|
1667
1703
|
}
|
|
1668
|
-
|
|
1704
|
+
// createIsParse returns null for non-matching shapes and bare `null`.
|
|
1705
|
+
const parsed = parseTrustedPublisherJson(stdout);
|
|
1669
1706
|
if (parsed === null) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
}
|
|
1675
|
-
if (isRecord(parsed)) {
|
|
1676
|
-
return [parseTrustedPublisher(parsed, packageName)];
|
|
1707
|
+
if (stdout.trim() === 'null') {
|
|
1708
|
+
return [];
|
|
1709
|
+
}
|
|
1710
|
+
throw new Error(`${packageName}: npm trust list returned invalid trusted publisher JSON.`);
|
|
1677
1711
|
}
|
|
1678
|
-
|
|
1712
|
+
return (Array.isArray(parsed) ? parsed : [parsed]).map((value) => ({
|
|
1713
|
+
id: value.id,
|
|
1714
|
+
type: value.type,
|
|
1715
|
+
file: value.file,
|
|
1716
|
+
repository: value.repository,
|
|
1717
|
+
}));
|
|
1679
1718
|
}
|
|
1680
1719
|
|
|
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
|
-
};
|
|
1720
|
+
interface TrustedPublisherJson {
|
|
1721
|
+
id: string;
|
|
1722
|
+
type: string;
|
|
1723
|
+
file?: string;
|
|
1724
|
+
repository?: string;
|
|
1696
1725
|
}
|
|
1697
1726
|
|
|
1727
|
+
const parseTrustedPublisherJson = typia.json.createIsParse<TrustedPublisherJson | TrustedPublisherJson[]>();
|
|
1728
|
+
|
|
1698
1729
|
async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
|
|
1699
1730
|
await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
|
|
1700
1731
|
}
|
|
@@ -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
|
}
|
package/src/wrangler/scaffold.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* .dev.vars.example), so it never drifts from what the worker declares.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { join
|
|
10
|
-
import {
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { ensureNx, ensureNxTargets, type NxTargetConfig, type PackageJson, writeJsonObject } from '../lib/json.js';
|
|
11
11
|
import { getWorkspacePackageManifests } from '../lib/workspace.js';
|
|
12
12
|
|
|
13
13
|
export interface ScaffoldOptions {
|
|
@@ -19,22 +19,21 @@ export interface ScaffoldOptions {
|
|
|
19
19
|
function resolveProject(
|
|
20
20
|
root: string,
|
|
21
21
|
project: string,
|
|
22
|
-
): { dir: string; label: string; packageJsonPath: string; json:
|
|
22
|
+
): { dir: string; label: string; packageJsonPath: string; json: PackageJson } {
|
|
23
23
|
const manifests = getWorkspacePackageManifests(root);
|
|
24
24
|
const match = manifests.find((m) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
return nxName === project || m.name === project || relative(root, m.path) === project;
|
|
25
|
+
const nxName = m.json.nx?.name;
|
|
26
|
+
return nxName === project || m.name === project || m.path === project;
|
|
28
27
|
});
|
|
29
28
|
if (!match) {
|
|
30
|
-
const known = manifests.map((m) =>
|
|
29
|
+
const known = manifests.map((m) => m.json.nx?.name ?? m.name).join(', ');
|
|
31
30
|
throw new Error(`project "${project}" not found. Known: ${known}`);
|
|
32
31
|
}
|
|
33
|
-
const dir = match.path;
|
|
32
|
+
const dir = join(root, match.path);
|
|
34
33
|
if (!existsSync(join(dir, 'wrangler.toml'))) {
|
|
35
34
|
throw new Error(`"${project}" has no wrangler.toml — not a wrangler project`);
|
|
36
35
|
}
|
|
37
|
-
return { dir, label:
|
|
36
|
+
return { dir, label: match.path, packageJsonPath: match.packageJsonPath, json: match.json };
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
/** The generated starter script — generic, manifest-driven, prompt-batteries included. */
|
|
@@ -226,13 +225,14 @@ export function scaffold(root: string, project: string, options: ScaffoldOptions
|
|
|
226
225
|
console.log(`${existed ? 'overwrote' : 'created'} ${label}/scripts/prepare-env.ts`);
|
|
227
226
|
|
|
228
227
|
// Wire the nx target (idempotent).
|
|
229
|
-
const nx =
|
|
230
|
-
const targets =
|
|
231
|
-
if (!
|
|
232
|
-
|
|
228
|
+
const nx = ensureNx(json);
|
|
229
|
+
const targets = ensureNxTargets(nx);
|
|
230
|
+
if (!targets['prepare-env']) {
|
|
231
|
+
const prepareEnvTarget: NxTargetConfig = {
|
|
233
232
|
executor: 'nx:run-commands',
|
|
234
233
|
options: { command: 'bun scripts/prepare-env.ts', cwd: '{projectRoot}' },
|
|
235
234
|
};
|
|
235
|
+
targets['prepare-env'] = prepareEnvTarget;
|
|
236
236
|
writeJsonObject(packageJsonPath, json);
|
|
237
237
|
console.log(`updated ${label}/package.json prepare-env target`);
|
|
238
238
|
}
|