@dbx-tools/projen 0.9.5 → 0.9.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/README.md +69 -17
- package/index.ts +7 -5
- package/package.json +9 -6
- package/src/_rust-project.ts +356 -0
- package/src/_rust-release-workflow.ts +954 -0
- package/src/_rust-release.ts +605 -0
- package/src/_task-command.ts +89 -0
- package/src/barrels.ts +12 -11
- package/src/bun-app.ts +96 -85
- package/src/bun-workflow.ts +60 -52
- package/src/engine-root.ts +5 -5
- package/src/openapi.ts +16 -9
- package/src/packages.ts +9 -9
- package/src/pnpm-workspace.ts +1 -1
- package/src/project-js.ts +19 -25
- package/src/project-py.ts +28 -23
- package/src/project-rs.ts +100 -1098
- package/src/projen-version.ts +9 -0
- package/src/publish.ts +3 -4
- package/src/release-catalog.ts +1 -1
- package/src/release.ts +212 -177
- package/src/scaffold.ts +1 -1
- package/src/uniffi.ts +7 -0
- package/src/watch.ts +2 -2
- package/src/workspace-version.ts +20 -15
- package/tasks/barrels.ts +4 -4
- package/tasks/build-rust-release.ts +68 -0
- package/tasks/bump.ts +3 -2
- package/tasks/clean.ts +3 -3
- package/tasks/local-publish.ts +21 -3
- package/tasks/openapi.ts +2 -2
- package/tasks/publish-npm.ts +12 -6
- package/tasks/publish-python.ts +2 -2
- package/tasks/publish-uniffi-local.ts +11 -15
- package/tasks/publish.ts +88 -88
- package/tasks/python-registry.ts +1 -1
- package/tasks/release-bootstrap.ts +2 -2
- package/tasks/release-plan.ts +1 -1
- package/tasks/release-please.ts +1 -1
- package/tasks/release-pr.ts +60 -115
- package/tasks/release-request.ts +8 -29
- package/tasks/release-summary-units.ts +2 -9
- package/tasks/release-summary.ts +35 -24
- package/tasks/release-version.ts +44 -0
- package/tasks/rust-release.d.ts +41 -0
- package/tasks/rust-release.mjs +1717 -0
- package/tasks/sync.ts +3 -3
- package/tasks/uniffi-python.js +1 -1
- package/tasks/uniffi.ts +7 -1
- package/tasks/version-check.ts +6 -5
package/tasks/barrels.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
import { sep } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { log,
|
|
4
|
+
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
5
5
|
import { generateBarrels } from "../src/barrels.ts";
|
|
6
6
|
import { recordedPackages } from "../src/packages.ts";
|
|
7
7
|
import { watchLoop, watchRoots } from "../src/watch.ts";
|
|
@@ -43,14 +43,14 @@ if (values.watch) {
|
|
|
43
43
|
if (dirs.size === 0) {
|
|
44
44
|
if (unowned.length) {
|
|
45
45
|
logger.warn(
|
|
46
|
-
`no recorded package owns ${
|
|
46
|
+
`no recorded package owns ${stringUtils.pluralize(unowned.length, "change")}; ` +
|
|
47
47
|
"run `bun run default` (or touch .projenrc.ts) to pick up a new package folder",
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
const n = generateBarrels({ dirs: [...dirs] });
|
|
53
|
-
if (n) logger.success(`rebuilt ${
|
|
53
|
+
if (n) logger.success(`rebuilt ${stringUtils.pluralize(n, "barrel")}`);
|
|
54
54
|
});
|
|
55
55
|
} else {
|
|
56
56
|
const dirs = Array.isArray(values.dir)
|
|
@@ -58,6 +58,6 @@ if (values.watch) {
|
|
|
58
58
|
: [];
|
|
59
59
|
const n = generateBarrels(dirs.length ? { dirs } : undefined);
|
|
60
60
|
logger.success(
|
|
61
|
-
n === 0 ? "barrels already up to date" : `updated ${
|
|
61
|
+
n === 0 ? "barrels already up to date" : `updated ${stringUtils.pluralize(n, "barrel")}`,
|
|
62
62
|
);
|
|
63
63
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Bundle the owning Rust release helper source into a standalone Node module. */
|
|
3
|
+
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
7
|
+
|
|
8
|
+
const root = resolve(import.meta.dirname, "..");
|
|
9
|
+
const temporary = mkdtempSync(join(tmpdir(), "dbx-rust-release-bundle-"));
|
|
10
|
+
try {
|
|
11
|
+
const bundled = join(temporary, "rust-release.mjs");
|
|
12
|
+
const declarations = join(temporary, "declarations");
|
|
13
|
+
exec.spawnSync(
|
|
14
|
+
process.execPath,
|
|
15
|
+
[
|
|
16
|
+
"build",
|
|
17
|
+
resolve(root, "src/_rust-release.ts"),
|
|
18
|
+
"--target=node",
|
|
19
|
+
"--format=esm",
|
|
20
|
+
`--outfile=${bundled}`,
|
|
21
|
+
],
|
|
22
|
+
{
|
|
23
|
+
cwd: root,
|
|
24
|
+
stdout: "inherit",
|
|
25
|
+
stderr: "inherit",
|
|
26
|
+
stdin: "ignore",
|
|
27
|
+
check: true,
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
const output = resolve(root, "tasks/rust-release.mjs");
|
|
31
|
+
const source = readFileSync(bundled, "utf8");
|
|
32
|
+
writeFileSync(
|
|
33
|
+
output,
|
|
34
|
+
`#!/usr/bin/env node\n// GENERATED from src/_rust-release.ts by tasks/build-rust-release.ts.\n${source.replace(/^#![^\n]*\n/, "")}`,
|
|
35
|
+
);
|
|
36
|
+
chmodSync(output, 0o755);
|
|
37
|
+
exec.spawnSync(
|
|
38
|
+
"bunx",
|
|
39
|
+
[
|
|
40
|
+
"tsc",
|
|
41
|
+
resolve(root, "src/_rust-release.ts"),
|
|
42
|
+
"--declaration",
|
|
43
|
+
"--emitDeclarationOnly",
|
|
44
|
+
"--module",
|
|
45
|
+
"ESNext",
|
|
46
|
+
"--moduleResolution",
|
|
47
|
+
"Bundler",
|
|
48
|
+
"--target",
|
|
49
|
+
"ES2022",
|
|
50
|
+
"--skipLibCheck",
|
|
51
|
+
"--outDir",
|
|
52
|
+
declarations,
|
|
53
|
+
],
|
|
54
|
+
{
|
|
55
|
+
cwd: root,
|
|
56
|
+
stdout: "inherit",
|
|
57
|
+
stderr: "inherit",
|
|
58
|
+
stdin: "ignore",
|
|
59
|
+
check: true,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
writeFileSync(
|
|
63
|
+
resolve(root, "tasks/rust-release.d.ts"),
|
|
64
|
+
readFileSync(join(declarations, "_rust-release.d.ts")),
|
|
65
|
+
);
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
68
|
+
}
|
package/tasks/bump.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* generated version surface. This task has no git, registry, or publication
|
|
5
5
|
* side effects; release preparation owns those operations.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
8
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
8
9
|
import { log } from "@dbx-tools/shared-core";
|
|
9
10
|
import { Command } from "commander";
|
|
10
11
|
import {
|
|
@@ -36,7 +37,7 @@ program
|
|
|
36
37
|
arch: ReleaseArch[];
|
|
37
38
|
synth: boolean;
|
|
38
39
|
}) => {
|
|
39
|
-
const root =
|
|
40
|
+
const root = projectUtils.root() ?? process.cwd();
|
|
40
41
|
const next = resolveNextVersion(root, [opts.prefix], opts.level);
|
|
41
42
|
const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
|
|
42
43
|
|
package/tasks/clean.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
import { relative } from "node:path";
|
|
3
|
-
import { log,
|
|
3
|
+
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
4
4
|
import { listGeneratedFiles, listNodeModulesDirs, removePaths } from "../src/clean.ts";
|
|
5
5
|
import { repoRoot, toPosix } from "../src/packages.ts";
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ const regenHint = (removedNodeModules: boolean): string =>
|
|
|
24
24
|
if (yes) {
|
|
25
25
|
const n = removePaths(targets);
|
|
26
26
|
logger.success(
|
|
27
|
-
`removed ${
|
|
27
|
+
`removed ${stringUtils.pluralize(n, "path")} (${files.length} generated + ${nodeModules.length} node_modules) - ${regenHint(nodeModules.length > 0)}`,
|
|
28
28
|
);
|
|
29
29
|
process.exit(0);
|
|
30
30
|
}
|
|
@@ -64,4 +64,4 @@ if (picked.length === 0) {
|
|
|
64
64
|
|
|
65
65
|
const removedNodeModules = picked.some((p) => nodeModules.includes(p));
|
|
66
66
|
const n = removePaths(picked);
|
|
67
|
-
clack.outro(`removed ${
|
|
67
|
+
clack.outro(`removed ${stringUtils.pluralize(n, "path")} - ${regenHint(removedNodeModules)}`);
|
package/tasks/local-publish.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import
|
|
6
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
7
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
7
8
|
import { log, net } from "@dbx-tools/shared-core";
|
|
8
9
|
import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
|
|
9
10
|
import { readDbxToolsConfig } from "../src/packages.ts";
|
|
@@ -18,13 +19,15 @@ export interface LocalPublishOptions {
|
|
|
18
19
|
readonly localPypi: string;
|
|
19
20
|
readonly pythonRoot?: string;
|
|
20
21
|
readonly localCargo: boolean;
|
|
22
|
+
/** Reuse the root TypeScript compile that immediately preceded local publication. */
|
|
23
|
+
readonly reuseValidatedNodeCompile?: boolean;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
function resolveLocalRegistry(value: string): string | undefined {
|
|
24
27
|
const trimmed = value.trim();
|
|
25
28
|
if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
|
|
26
29
|
if (trimmed.toLowerCase() === "auto") {
|
|
27
|
-
const registry =
|
|
30
|
+
const registry = projectUtils.npmRegistry();
|
|
28
31
|
return registry && net.isLoopbackHost(registry) ? registry.href : undefined;
|
|
29
32
|
}
|
|
30
33
|
return trimmed;
|
|
@@ -45,6 +48,21 @@ function localCargoRegistry(): string | undefined {
|
|
|
45
48
|
return undefined;
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** Arguments for the workspace npm publisher used by local release preflight. */
|
|
52
|
+
export function localNodePublishArguments(
|
|
53
|
+
publishScript: string,
|
|
54
|
+
options: Pick<LocalPublishOptions, "version" | "reuseValidatedNodeCompile">,
|
|
55
|
+
registry: string,
|
|
56
|
+
): string[] {
|
|
57
|
+
return [
|
|
58
|
+
publishScript,
|
|
59
|
+
options.version,
|
|
60
|
+
"--registry",
|
|
61
|
+
registry,
|
|
62
|
+
...(options.reuseValidatedNodeCompile ? ["--skip-compile"] : []),
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
/** Publish the exact release candidate to local npm, PyPI, and Cargo mirrors. */
|
|
49
67
|
export async function publishLocalRelease(options: LocalPublishOptions): Promise<void> {
|
|
50
68
|
const localRegistry = resolveLocalRegistry(options.localRegistry);
|
|
@@ -68,7 +86,7 @@ export async function publishLocalRelease(options: LocalPublishOptions): Promise
|
|
|
68
86
|
logger.info(`publishing ${options.version} to local registry ${localRegistry}`);
|
|
69
87
|
localPublishes.push(
|
|
70
88
|
exec
|
|
71
|
-
.spawn(process.execPath,
|
|
89
|
+
.spawn(process.execPath, localNodePublishArguments(publishScript, options, localRegistry), {
|
|
72
90
|
cwd: options.root,
|
|
73
91
|
stdout: "inherit",
|
|
74
92
|
stderr: "inherit",
|
package/tasks/openapi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
|
-
import { log,
|
|
2
|
+
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
3
3
|
import { generateBarrels } from "../src/barrels.ts";
|
|
4
4
|
import { generateOpenapi, isTsoaController } from "../src/openapi.ts";
|
|
5
5
|
import { runSynth } from "../src/scaffold.ts";
|
|
@@ -16,7 +16,7 @@ if (process.argv.includes("--watch")) {
|
|
|
16
16
|
const dirs = await generateOpenapi();
|
|
17
17
|
if (dirs.length) {
|
|
18
18
|
generateBarrels({ dirs });
|
|
19
|
-
logger.success(`regenerated openapi (${
|
|
19
|
+
logger.success(`regenerated openapi (${stringUtils.pluralize(dirs.length, "package")})`);
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
} else {
|
package/tasks/publish-npm.ts
CHANGED
|
@@ -12,14 +12,15 @@ import {
|
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { join, relative, resolve } from "node:path";
|
|
15
|
-
import
|
|
15
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
16
16
|
import { log } from "@dbx-tools/shared-core";
|
|
17
17
|
import { Command } from "commander";
|
|
18
18
|
|
|
19
19
|
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
20
|
-
const logger = log.logger("
|
|
20
|
+
const logger = log.logger("projen:publish-npm");
|
|
21
21
|
|
|
22
22
|
export interface NpmReleaseIdentity {
|
|
23
|
+
readonly access?: "public" | "restricted";
|
|
23
24
|
readonly contentDigest?: string;
|
|
24
25
|
readonly integrity?: string;
|
|
25
26
|
readonly name: string;
|
|
@@ -101,7 +102,7 @@ export async function publishedNpmRelease(
|
|
|
101
102
|
if (!archive.ok) {
|
|
102
103
|
throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
|
|
103
104
|
}
|
|
104
|
-
const temp = mkdtempSync(join(tmpdir(), "
|
|
105
|
+
const temp = mkdtempSync(join(tmpdir(), "projen-published-npm-"));
|
|
105
106
|
const path = join(temp, "package.tgz");
|
|
106
107
|
try {
|
|
107
108
|
writeFileSync(path, Buffer.from(await archive.arrayBuffer()));
|
|
@@ -132,13 +133,19 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
|
132
133
|
}
|
|
133
134
|
const manifest = JSON.parse(result.stdout) as {
|
|
134
135
|
name?: string;
|
|
136
|
+
publishConfig?: { access?: unknown };
|
|
135
137
|
repository?: unknown;
|
|
136
138
|
version?: string;
|
|
137
139
|
};
|
|
138
140
|
if (!manifest.name || !manifest.version) {
|
|
139
141
|
throw new Error(`npm archive has no package name or version: ${path}`);
|
|
140
142
|
}
|
|
143
|
+
const access = manifest.publishConfig?.access;
|
|
144
|
+
if (access !== undefined && access !== "public" && access !== "restricted") {
|
|
145
|
+
throw new Error(`npm archive has invalid publishConfig.access: ${String(access)}`);
|
|
146
|
+
}
|
|
141
147
|
return {
|
|
148
|
+
...(access ? { access } : {}),
|
|
142
149
|
contentDigest: npmArchiveContentDigest(path),
|
|
143
150
|
integrity: `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`,
|
|
144
151
|
name: manifest.name,
|
|
@@ -149,7 +156,7 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
|
149
156
|
|
|
150
157
|
/** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
|
|
151
158
|
export function npmArchiveContentDigest(path: string): string {
|
|
152
|
-
const temp = mkdtempSync(join(tmpdir(), "
|
|
159
|
+
const temp = mkdtempSync(join(tmpdir(), "projen-npm-content-"));
|
|
153
160
|
try {
|
|
154
161
|
exec.spawnSync("tar", ["-xzf", path, "-C", temp], {
|
|
155
162
|
cwd: process.cwd(),
|
|
@@ -237,8 +244,7 @@ export async function publishNpmArchives(options: {
|
|
|
237
244
|
[
|
|
238
245
|
"publish",
|
|
239
246
|
archive,
|
|
240
|
-
"--access",
|
|
241
|
-
"public",
|
|
247
|
+
...(local.access ? ["--access", local.access] : []),
|
|
242
248
|
...(options.registry ? ["--registry", options.registry] : []),
|
|
243
249
|
...(options.dryRun ? ["--dry-run"] : []),
|
|
244
250
|
],
|
package/tasks/publish-python.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { basename, join, resolve } from "node:path";
|
|
14
|
-
import
|
|
14
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
import { parse, stringify } from "smol-toml";
|
|
17
17
|
import { pythonProjectInfo, stampPythonProject } from "./uniffi-python.js";
|
|
@@ -111,7 +111,7 @@ export function publishPythonProjects(options: {
|
|
|
111
111
|
readonly plan?: ReleasePlan;
|
|
112
112
|
}): void {
|
|
113
113
|
const root = resolve(options.root);
|
|
114
|
-
const output = mkdtempSync(join(tmpdir(), "
|
|
114
|
+
const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
|
|
115
115
|
const planned = new Map(
|
|
116
116
|
options.plan?.pythonPackages.map((pkg) => [pkg.path.replace(/^.*\//, ""), pkg.version]) ?? [],
|
|
117
117
|
);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
2
|
import { chmodSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { arch, platform } from "node:os";
|
|
5
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -7,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
6
|
import { parseArgs } from "node:util";
|
|
8
7
|
import { log } from "@dbx-tools/shared-core";
|
|
9
8
|
import { parse, stringify } from "smol-toml";
|
|
9
|
+
import { captureTaskCommand, runTaskCommand, taskCommandSucceeds } from "../src/_task-command.ts";
|
|
10
10
|
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
11
11
|
import type { RustBindingMapping, RustWorkspaceMapping } from "../src/project-rs.ts";
|
|
12
12
|
|
|
@@ -22,17 +22,13 @@ const parsed = parseArgs({
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
function run(command: string, args: string[], capture = false): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
stdio: capture ? "pipe" : "inherit",
|
|
29
|
-
});
|
|
30
|
-
if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
|
|
31
|
-
return capture ? String(result.stdout).trim() : "";
|
|
25
|
+
if (capture) return captureTaskCommand(repoRoot, command, args, { check: true });
|
|
26
|
+
runTaskCommand(repoRoot, command, args);
|
|
27
|
+
return "";
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
function commandAvailable(command: string): boolean {
|
|
35
|
-
return
|
|
31
|
+
return taskCommandSucceeds(repoRoot, command, ["--version"]);
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
function rustHost(): string {
|
|
@@ -90,12 +86,12 @@ function artifacts(directory: string, suffix: string): string[] {
|
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
function cargoVersionExists(crateName: string, version: string, registry: string): boolean {
|
|
93
|
-
return (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
89
|
+
return taskCommandSucceeds(repoRoot, "cargo", [
|
|
90
|
+
"info",
|
|
91
|
+
`${crateName}@${version}`,
|
|
92
|
+
"--registry",
|
|
93
|
+
registry,
|
|
94
|
+
]);
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
function publishCargo(config: RustWorkspaceMapping, registry: string, version: string): void {
|
package/tasks/publish.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/**
|
|
3
|
-
* `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run]`
|
|
3
|
+
* `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]`
|
|
4
4
|
* - verify every workspace member and the Bun lock carry the release version,
|
|
5
|
-
* then publish each package owned by the standard Node release
|
|
5
|
+
* then pack and publish each package owned by the standard Node release.
|
|
6
6
|
*
|
|
7
7
|
* Bun has no `pnpm -r publish`, so this loop is the recursive-publish stand-in.
|
|
8
8
|
* It leans on native bun for everything bun already does:
|
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
* its `#!/usr/bin/env node` shebang, node chokes on the `.ts`
|
|
27
27
|
* (ERR_UNKNOWN_FILE_EXTENSION). We merge `publishConfig` onto the top-level
|
|
28
28
|
* manifest before packing so the tarball advertises the compiled `lib/` tree;
|
|
29
|
-
* - **compiled output** is emitted once, before
|
|
29
|
+
* - **compiled output** is emitted once, before packing, by one root-level
|
|
30
30
|
* filtered `bun run` that fans out to every publishable member in parallel.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `prepack`. Packages retain
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* Every package is then packed once with lifecycle scripts disabled. The
|
|
32
|
+
* exact validated archive is passed to `bun publish`, so upload never
|
|
33
|
+
* repacks or repeats a member's `prepack`. Packages retain `prepack` for
|
|
34
|
+
* standalone publishes.
|
|
35
|
+
* - **release recovery** compares each packed archive's integrity and
|
|
36
|
+
* repository identity with registry metadata. A matching immutable version
|
|
37
|
+
* is skipped, while any mismatch fails the retry.
|
|
37
38
|
*
|
|
38
39
|
* `--dry-run` forwards to `bun publish`: it packs + validates
|
|
39
40
|
* but uploads nothing, so the `release` workflow is testable end-to-end via a
|
|
@@ -63,8 +64,7 @@ import {
|
|
|
63
64
|
} from "node:fs";
|
|
64
65
|
import { tmpdir } from "node:os";
|
|
65
66
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
66
|
-
import {
|
|
67
|
-
import { log } from "@dbx-tools/shared-core";
|
|
67
|
+
import { asyncUtils, log } from "@dbx-tools/shared-core";
|
|
68
68
|
import ts from "typescript";
|
|
69
69
|
import { parse } from "yaml";
|
|
70
70
|
import {
|
|
@@ -73,9 +73,11 @@ import {
|
|
|
73
73
|
publishedNpmRelease,
|
|
74
74
|
readNpmArchiveIdentity,
|
|
75
75
|
} from "./publish-npm.ts";
|
|
76
|
+
import { runTaskCommand, runTaskCommandAsync } from "../src/_task-command.ts";
|
|
77
|
+
import { toPosix } from "../src/packages.ts";
|
|
76
78
|
import type { ReleasePlan } from "../src/release-plan.ts";
|
|
77
79
|
|
|
78
|
-
const logger = log.logger("
|
|
80
|
+
const logger = log.logger("projen:publish");
|
|
79
81
|
|
|
80
82
|
/** A manifest's on-disk bytes + mode, captured before this task edits it. */
|
|
81
83
|
interface ManifestBackup {
|
|
@@ -115,7 +117,7 @@ function manifestsMatchVersions(
|
|
|
115
117
|
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
116
118
|
version?: string;
|
|
117
119
|
};
|
|
118
|
-
const path = dir.slice(resolve(root).length + 1)
|
|
120
|
+
const path = toPosix(dir.slice(resolve(root).length + 1));
|
|
119
121
|
return pkg.version === expected.get(path);
|
|
120
122
|
});
|
|
121
123
|
}
|
|
@@ -134,10 +136,7 @@ function lockfileMatchesManifestVersions(root: string, members: readonly string[
|
|
|
134
136
|
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
135
137
|
version?: string;
|
|
136
138
|
};
|
|
137
|
-
const relative = dir
|
|
138
|
-
.slice(resolve(root).length + 1)
|
|
139
|
-
.split("\\")
|
|
140
|
-
.join("/");
|
|
139
|
+
const relative = toPosix(dir.slice(resolve(root).length + 1));
|
|
141
140
|
return lock.workspaces?.[relative]?.version === manifest.version;
|
|
142
141
|
});
|
|
143
142
|
} catch {
|
|
@@ -145,51 +144,6 @@ function lockfileMatchesManifestVersions(root: string, members: readonly string[
|
|
|
145
144
|
}
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
/** Spawn `command` in `cwd` with `PATH` overridden, failing the task on non-zero. */
|
|
149
|
-
function run(cwd: string, command: string, args: string[], path: string): void {
|
|
150
|
-
const executable = command === "bun" && process.versions.bun ? process.execPath : command;
|
|
151
|
-
exec.spawnSync(executable, args, {
|
|
152
|
-
cwd,
|
|
153
|
-
stdout: "inherit",
|
|
154
|
-
stderr: "inherit",
|
|
155
|
-
stdin: "ignore",
|
|
156
|
-
check: true,
|
|
157
|
-
env: { ...process.env, PATH: path },
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Asynchronous counterpart used for bounded parallel stamping and publishing. */
|
|
162
|
-
async function runAsync(cwd: string, command: string, args: string[], path: string): Promise<void> {
|
|
163
|
-
const executable = command === "bun" && process.versions.bun ? process.execPath : command;
|
|
164
|
-
await exec.spawn(executable, args, {
|
|
165
|
-
cwd,
|
|
166
|
-
stdout: "inherit",
|
|
167
|
-
stderr: "inherit",
|
|
168
|
-
stdin: "ignore",
|
|
169
|
-
check: true,
|
|
170
|
-
env: { ...process.env, PATH: path },
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Run independent jobs with a small fixed worker pool. */
|
|
175
|
-
async function runConcurrent<T>(
|
|
176
|
-
values: readonly T[],
|
|
177
|
-
concurrency: number,
|
|
178
|
-
worker: (value: T) => Promise<void>,
|
|
179
|
-
): Promise<void> {
|
|
180
|
-
let next = 0;
|
|
181
|
-
const results = await Promise.allSettled(
|
|
182
|
-
Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
183
|
-
while (next < values.length) {
|
|
184
|
-
const value = values[next++];
|
|
185
|
-
await worker(value);
|
|
186
|
-
}
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
const failure = results.find((result) => result.status === "rejected");
|
|
190
|
-
if (failure?.status === "rejected") throw failure.reason;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
147
|
/**
|
|
194
148
|
* Make a projen-readonly manifest writable so the publishConfig projection can
|
|
195
149
|
* edit it, recording its original bytes and mode for {@link restoreManifests}
|
|
@@ -228,6 +182,14 @@ function restoreManifests(): void {
|
|
|
228
182
|
/** Entry-point fields projen writes as `.ts` source in-repo and rewrites to `lib/` for publish. */
|
|
229
183
|
const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
|
|
230
184
|
|
|
185
|
+
/** Compiled files referenced by a publishConfig entry-point tree. */
|
|
186
|
+
function compiledPublishTargets(value: unknown): string[] {
|
|
187
|
+
if (typeof value === "string") return value.startsWith("./lib/") ? [value] : [];
|
|
188
|
+
if (Array.isArray(value)) return value.flatMap(compiledPublishTargets);
|
|
189
|
+
if (!value || typeof value !== "object") return [];
|
|
190
|
+
return Object.values(value).flatMap(compiledPublishTargets);
|
|
191
|
+
}
|
|
192
|
+
|
|
231
193
|
/**
|
|
232
194
|
* Fold a package's `publishConfig` entry-point fields onto the top-level manifest,
|
|
233
195
|
* the way pnpm/npm do at pack time but `bun publish` does NOT (see the module
|
|
@@ -272,13 +234,14 @@ const plan =
|
|
|
272
234
|
: undefined;
|
|
273
235
|
if (!version && !plan) {
|
|
274
236
|
logger.error(
|
|
275
|
-
"usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--exclude <dir>] [--dry-run]",
|
|
237
|
+
"usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]",
|
|
276
238
|
);
|
|
277
239
|
process.exit(1);
|
|
278
240
|
}
|
|
279
241
|
const registryIdx = rest.indexOf("--registry");
|
|
280
242
|
const registry = registryIdx >= 0 ? rest[registryIdx + 1] : undefined;
|
|
281
243
|
const dryRun = rest.includes("--dry-run");
|
|
244
|
+
const skipCompile = rest.includes("--skip-compile");
|
|
282
245
|
const concurrencyIdx = rest.indexOf("--concurrency");
|
|
283
246
|
const parsedConcurrency = Number(concurrencyIdx >= 0 ? rest[concurrencyIdx + 1] : 4);
|
|
284
247
|
if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
|
|
@@ -302,15 +265,10 @@ const allMembers = workspaceMembers(root)
|
|
|
302
265
|
const expectedVersions = new Map(
|
|
303
266
|
plan
|
|
304
267
|
? plan.nodePackages.map((pkg) => [pkg.path, pkg.version] as const)
|
|
305
|
-
: allMembers.map((dir) => [
|
|
306
|
-
dir.slice(resolve(root).length + 1).replaceAll("\\", "/"),
|
|
307
|
-
version!,
|
|
308
|
-
]),
|
|
268
|
+
: allMembers.map((dir) => [toPosix(dir.slice(resolve(root).length + 1)), version!]),
|
|
309
269
|
);
|
|
310
270
|
const members = plan
|
|
311
|
-
? allMembers.filter((dir) =>
|
|
312
|
-
expectedVersions.has(dir.slice(resolve(root).length + 1).replaceAll("\\", "/")),
|
|
313
|
-
)
|
|
271
|
+
? allMembers.filter((dir) => expectedVersions.has(toPosix(dir.slice(resolve(root).length + 1))))
|
|
314
272
|
: allMembers;
|
|
315
273
|
|
|
316
274
|
if (!manifestsMatchVersions(root, members, expectedVersions)) {
|
|
@@ -328,12 +286,10 @@ if (lockfileMatchesManifestVersions(root, allMembers)) {
|
|
|
328
286
|
} else {
|
|
329
287
|
if (existsSync(lockfile)) rmSync(lockfile);
|
|
330
288
|
logger.info("refreshing lockfile so workspace deps resolve to the release version");
|
|
331
|
-
|
|
289
|
+
runTaskCommand(root, "bun", ["install"], { env: { ...process.env, PATH: path } });
|
|
332
290
|
}
|
|
333
291
|
|
|
334
292
|
const publishArgs = [
|
|
335
|
-
"--access",
|
|
336
|
-
"public",
|
|
337
293
|
"--ignore-scripts",
|
|
338
294
|
...(registry ? ["--registry", registry] : []),
|
|
339
295
|
...(dryRun ? ["--dry-run"] : []),
|
|
@@ -343,6 +299,8 @@ const publishable: Array<{
|
|
|
343
299
|
name: string;
|
|
344
300
|
version: string;
|
|
345
301
|
compile: boolean;
|
|
302
|
+
compiledTargets: string[];
|
|
303
|
+
access?: "public" | "restricted";
|
|
346
304
|
}> = [];
|
|
347
305
|
for (const dir of members) {
|
|
348
306
|
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
@@ -350,7 +308,7 @@ for (const dir of members) {
|
|
|
350
308
|
version?: string;
|
|
351
309
|
private?: boolean;
|
|
352
310
|
dbxToolsConfig?: { uniffi?: boolean };
|
|
353
|
-
|
|
311
|
+
publishConfig?: { access?: unknown };
|
|
354
312
|
};
|
|
355
313
|
if (pkg.private) {
|
|
356
314
|
logger.info(`skip private ${pkg.name ?? dirname(dir)}`);
|
|
@@ -361,18 +319,46 @@ for (const dir of members) {
|
|
|
361
319
|
continue;
|
|
362
320
|
}
|
|
363
321
|
if (!pkg.version) throw new Error(`Missing package version for ${pkg.name ?? dirname(dir)}`);
|
|
322
|
+
const access = pkg.publishConfig?.access;
|
|
323
|
+
if (access !== undefined && access !== "public" && access !== "restricted") {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`${pkg.name ?? dirname(dir)} has invalid publishConfig.access ${String(access)}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const compiledTargets = [...new Set(compiledPublishTargets(pkg.publishConfig))];
|
|
364
329
|
publishable.push({
|
|
365
330
|
dir,
|
|
366
331
|
name: pkg.name ?? dirname(dir),
|
|
367
332
|
version: pkg.version,
|
|
368
|
-
compile:
|
|
333
|
+
compile: compiledTargets.length > 0,
|
|
334
|
+
compiledTargets,
|
|
335
|
+
...(access ? { access } : {}),
|
|
369
336
|
});
|
|
370
337
|
}
|
|
371
338
|
|
|
372
339
|
const compiled = publishable.filter((pkg) => pkg.compile);
|
|
373
340
|
if (compiled.length > 0) {
|
|
374
|
-
|
|
375
|
-
|
|
341
|
+
if (skipCompile) {
|
|
342
|
+
const missing = compiled.flatMap((pkg) =>
|
|
343
|
+
pkg.compiledTargets
|
|
344
|
+
.filter((target) => !existsSync(resolve(pkg.dir, target)))
|
|
345
|
+
.map((target) => `${pkg.name}:${target}`),
|
|
346
|
+
);
|
|
347
|
+
if (missing.length > 0) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`--skip-compile requires validated compiled output; missing ${missing.join(", ")}`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
logger.info(`reusing validated compiled output for ${compiled.length} publishable packages`);
|
|
353
|
+
} else {
|
|
354
|
+
logger.info(`compiling ${compiled.length} publishable packages from the workspace root`);
|
|
355
|
+
runTaskCommand(
|
|
356
|
+
root,
|
|
357
|
+
"bun",
|
|
358
|
+
["run", ...compiled.flatMap((pkg) => ["--filter", pkg.name]), "compile"],
|
|
359
|
+
{ env: { ...process.env, PATH: path } },
|
|
360
|
+
);
|
|
361
|
+
}
|
|
376
362
|
}
|
|
377
363
|
|
|
378
364
|
// Compile against workspace source exports first. Switching manifests to their
|
|
@@ -387,9 +373,10 @@ for (const { dir } of publishable) {
|
|
|
387
373
|
logger.info(
|
|
388
374
|
`${dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
|
|
389
375
|
);
|
|
390
|
-
await
|
|
391
|
-
|
|
392
|
-
|
|
376
|
+
await asyncUtils.mapConcurrent(
|
|
377
|
+
publishable,
|
|
378
|
+
async ({ dir, name, version: packageVersion, access }) => {
|
|
379
|
+
const packed = mkdtempSync(join(tmpdir(), "projen-npm-release-"));
|
|
393
380
|
try {
|
|
394
381
|
const archive = packNpmPackage(dir, packed, path);
|
|
395
382
|
const local = readNpmArchiveIdentity(archive);
|
|
@@ -398,16 +385,29 @@ await runConcurrent(publishable, concurrency, async ({ dir, name, version: packa
|
|
|
398
385
|
`Packed npm identity ${local.name}@${local.version} does not match ${name}@${packageVersion}`,
|
|
399
386
|
);
|
|
400
387
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
388
|
+
if (local.access !== access) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Packed npm access ${String(local.access)} does not match ${String(access)} for ${name}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (!dryRun) {
|
|
394
|
+
const published = await publishedNpmRelease(local.name, local.version, registry);
|
|
395
|
+
if (npmReleaseMatches(local, published)) {
|
|
396
|
+
logger.info(`skip published ${name} @ ${packageVersion}`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
405
399
|
}
|
|
400
|
+
logger.info(`${dryRun ? "dry-run publishing" : "publishing"} ${name} @ ${packageVersion}`);
|
|
401
|
+
await runTaskCommandAsync(
|
|
402
|
+
dir,
|
|
403
|
+
"bun",
|
|
404
|
+
["publish", ...(access ? ["--access", access] : []), ...publishArgs, archive],
|
|
405
|
+
{ env: { ...process.env, PATH: path } },
|
|
406
|
+
);
|
|
406
407
|
} finally {
|
|
407
408
|
rmSync(packed, { recursive: true, force: true });
|
|
408
409
|
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
});
|
|
410
|
+
},
|
|
411
|
+
{ concurrency, errorMode: "settle" },
|
|
412
|
+
);
|
|
413
413
|
logger.success(`${dryRun ? "dry-run: packed" : "published"} ${publishable.length} packages`);
|