@dbx-tools/projen 0.9.153 → 0.9.157
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 +44 -52
- package/index.ts +43 -21
- package/package.json +8 -8
- package/src/_task-command.ts +7 -7
- package/src/appkit-workspace.ts +278 -104
- package/src/barrels.ts +147 -259
- package/src/bun-app.ts +54 -10
- package/src/clean.ts +6 -6
- package/src/engine-root.ts +4 -4
- package/src/mixin.ts +0 -2
- package/src/module-exports.ts +4 -5
- package/src/npm-packaging.ts +128 -0
- package/src/npm-release.ts +200 -0
- package/src/packages.ts +11 -11
- package/src/project-js.ts +209 -103
- package/src/project-predicate.ts +5 -12
- package/src/project-py.ts +8 -6
- package/src/project.ts +7 -14
- package/src/publish.ts +3 -3
- package/src/python-node-bindings.ts +16 -12
- package/src/python-packaging.ts +177 -0
- package/src/release-context.ts +4 -0
- package/src/release-options.ts +2 -2
- package/src/release.ts +133 -13
- package/src/shared-core-usage.ts +17 -4
- package/src/tags.ts +10 -9
- package/src/watch.ts +7 -9
- package/src/workspace-lock.ts +2 -2
- package/src/workspace-manifest.ts +2 -2
- package/tasks/appkit-deploy.ts +35 -15
- package/tasks/barrels.ts +6 -9
- package/tasks/bump.ts +1 -1
- package/tasks/clean.ts +3 -3
- package/tasks/cli.ts +3 -3
- package/tasks/compile-workspace.ts +8 -8
- package/tasks/dev-watch.ts +11 -11
- package/tasks/local-publish.ts +6 -6
- package/tasks/publish-npm.ts +11 -317
- package/tasks/publish-python.ts +2 -180
- package/tasks/publish.ts +6 -11
- package/tasks/python-node-bindings.ts +2 -2
- package/tasks/python-registry.ts +6 -6
- package/tasks/python-sync.ts +2 -3
- package/tasks/release-version.ts +1 -1
- package/tasks/release.ts +11 -11
- package/tasks/sync.ts +2 -2
- package/tasks/test-workspace.ts +9 -9
- package/tasks/verify-publications.ts +273 -0
- package/tasks/version-check.ts +5 -5
- /package/{tasks → src}/python-release.ts +0 -0
package/tasks/publish-npm.ts
CHANGED
|
@@ -1,214 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/** Idempotent npm archive publication for release recovery. */
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
lstatSync,
|
|
7
|
-
mkdtempSync,
|
|
8
|
-
readFileSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
readlinkSync,
|
|
11
|
-
rmSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import { join, relative, resolve } from "node:path";
|
|
16
|
-
import * as exec from "@dbx-tools/core/exec";
|
|
17
|
-
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
18
|
-
import { asyncUtils, log } from "@dbx-tools/shared-core";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { log, mapConcurrent } from "@dbx-tools/shared-core";
|
|
19
6
|
import { z } from "zod";
|
|
20
7
|
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
21
8
|
import { TaskConcurrencyOptionSchema, TaskDryRunOptionSchema } from "./options.ts";
|
|
22
9
|
import { runTaskCommandAsync } from "../src/_task-command.ts";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
import {
|
|
11
|
+
npmReleaseMatches,
|
|
12
|
+
publishedNpmRelease,
|
|
13
|
+
readNpmArchiveIdentity,
|
|
14
|
+
readNpmArchiveManifest,
|
|
15
|
+
} from "../src/npm-release.ts";
|
|
26
16
|
|
|
27
|
-
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
28
17
|
const DEFAULT_PUBLISH_CONCURRENCY = 4;
|
|
29
18
|
const logger = log.logger("projen:publish-npm");
|
|
30
19
|
|
|
31
|
-
/** Registry and archive identity used to verify idempotent npm publication. */
|
|
32
|
-
export interface NpmReleaseIdentity {
|
|
33
|
-
readonly access?: "public" | "restricted";
|
|
34
|
-
readonly contentDigest?: string;
|
|
35
|
-
readonly integrity?: string;
|
|
36
|
-
readonly name: string;
|
|
37
|
-
readonly repository?: unknown;
|
|
38
|
-
readonly version: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface NpmArchiveManifest {
|
|
42
|
-
readonly [key: string]: unknown;
|
|
43
|
-
readonly dependencies?: Readonly<Record<string, string>>;
|
|
44
|
-
readonly optionalDependencies?: Readonly<Record<string, string>>;
|
|
45
|
-
readonly name?: string;
|
|
46
|
-
readonly publishConfig?: { readonly access?: unknown };
|
|
47
|
-
readonly repository?: unknown;
|
|
48
|
-
readonly version?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Transform applied to a packed npm manifest before archive verification. */
|
|
52
|
-
export type NpmArchiveManifestTransform = (
|
|
53
|
-
manifest: NpmArchiveManifest,
|
|
54
|
-
) => Readonly<Record<string, unknown>>;
|
|
55
|
-
|
|
56
|
-
/** Entry-point fields projected from publishConfig into a packed manifest. */
|
|
57
|
-
const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
|
|
58
|
-
/**
|
|
59
|
-
* Fold compiled publish entry points onto an archive manifest without changing
|
|
60
|
-
* the source checkout.
|
|
61
|
-
*/
|
|
62
|
-
export function applyPublishConfig(
|
|
63
|
-
source: Readonly<Record<string, unknown>>,
|
|
64
|
-
): Readonly<Record<string, unknown>> {
|
|
65
|
-
const manifest = { ...source };
|
|
66
|
-
const publishConfig = manifest.publishConfig as Record<string, unknown> | undefined;
|
|
67
|
-
if (!publishConfig) return manifest;
|
|
68
|
-
for (const field of PUBLISH_CONFIG_ENTRY_FIELDS) {
|
|
69
|
-
if (field in publishConfig) manifest[field] = publishConfig[field];
|
|
70
|
-
}
|
|
71
|
-
return manifest;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function archiveFileEntries(directory: string, prefix = ""): string[] {
|
|
75
|
-
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
76
|
-
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
77
|
-
return entry.isDirectory()
|
|
78
|
-
? archiveFileEntries(join(directory, entry.name), relativePath)
|
|
79
|
-
: [relativePath];
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function normalizedRepository(value: unknown): string | undefined {
|
|
84
|
-
const url =
|
|
85
|
-
typeof value === "string"
|
|
86
|
-
? value
|
|
87
|
-
: value && typeof value === "object" && "url" in value
|
|
88
|
-
? String(value.url)
|
|
89
|
-
: undefined;
|
|
90
|
-
return url
|
|
91
|
-
?.replace(/^git\+/, "")
|
|
92
|
-
.replace(/\.git$/, "")
|
|
93
|
-
.replace(/\/$/, "");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function registryUrl(registry: string, name: string, version: string): string {
|
|
97
|
-
return `${registry.replace(/\/$/, "")}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Verify that registry metadata represents the exact locally packed release. */
|
|
101
|
-
export function npmReleaseMatches(
|
|
102
|
-
local: NpmReleaseIdentity,
|
|
103
|
-
published: NpmReleaseIdentity | undefined,
|
|
104
|
-
): boolean {
|
|
105
|
-
if (!published) return false;
|
|
106
|
-
if (published.name !== local.name || published.version !== local.version) {
|
|
107
|
-
throw new Error(`Published npm identity does not match ${local.name}@${local.version}`);
|
|
108
|
-
}
|
|
109
|
-
const localRepository = normalizedRepository(local.repository);
|
|
110
|
-
const publishedRepository = normalizedRepository(published.repository);
|
|
111
|
-
if (localRepository && publishedRepository && localRepository !== publishedRepository) {
|
|
112
|
-
throw new Error(`Published npm repository does not match ${local.name}@${local.version}`);
|
|
113
|
-
}
|
|
114
|
-
if (
|
|
115
|
-
local.contentDigest &&
|
|
116
|
-
published.contentDigest &&
|
|
117
|
-
local.contentDigest !== published.contentDigest
|
|
118
|
-
) {
|
|
119
|
-
throw new Error(`Published npm content does not match ${local.name}@${local.version}`);
|
|
120
|
-
}
|
|
121
|
-
if (
|
|
122
|
-
!(local.contentDigest && published.contentDigest) &&
|
|
123
|
-
local.integrity &&
|
|
124
|
-
published.integrity !== local.integrity
|
|
125
|
-
) {
|
|
126
|
-
throw new Error(`Published npm integrity does not match ${local.name}@${local.version}`);
|
|
127
|
-
}
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Read one published npm release identity, or `undefined` when it is absent. */
|
|
132
|
-
export async function publishedNpmRelease(
|
|
133
|
-
name: string,
|
|
134
|
-
version: string,
|
|
135
|
-
registry = process.env.NPM_CONFIG_REGISTRY ?? DEFAULT_REGISTRY,
|
|
136
|
-
): Promise<NpmReleaseIdentity | undefined> {
|
|
137
|
-
const response = await fetch(registryUrl(registry, name, version), {
|
|
138
|
-
headers: { accept: "application/json" },
|
|
139
|
-
});
|
|
140
|
-
if (response.status === 404) return undefined;
|
|
141
|
-
if (!response.ok) {
|
|
142
|
-
throw new Error(`npm registry lookup failed for ${name}@${version}: ${response.status}`);
|
|
143
|
-
}
|
|
144
|
-
const metadata = (await response.json()) as {
|
|
145
|
-
dist?: { integrity?: string; tarball?: string };
|
|
146
|
-
name?: string;
|
|
147
|
-
repository?: unknown;
|
|
148
|
-
version?: string;
|
|
149
|
-
};
|
|
150
|
-
if (!metadata.name || !metadata.version) {
|
|
151
|
-
throw new Error(`npm registry returned an incomplete identity for ${name}@${version}`);
|
|
152
|
-
}
|
|
153
|
-
let contentDigest: string | undefined;
|
|
154
|
-
if (metadata.dist?.tarball) {
|
|
155
|
-
const archive = await fetch(metadata.dist.tarball);
|
|
156
|
-
if (!archive.ok) {
|
|
157
|
-
throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
|
|
158
|
-
}
|
|
159
|
-
const temp = mkdtempSync(join(tmpdir(), "projen-published-npm-"));
|
|
160
|
-
const path = join(temp, "package.tgz");
|
|
161
|
-
try {
|
|
162
|
-
writeFileSync(path, Buffer.from(await archive.arrayBuffer()));
|
|
163
|
-
contentDigest = npmArchiveContentDigest(path);
|
|
164
|
-
} finally {
|
|
165
|
-
rmSync(temp, { recursive: true, force: true });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
...(contentDigest ? { contentDigest } : {}),
|
|
170
|
-
integrity: metadata.dist?.integrity,
|
|
171
|
-
name: metadata.name,
|
|
172
|
-
repository: metadata.repository,
|
|
173
|
-
version: metadata.version,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function readNpmArchiveManifest(path: string): NpmArchiveManifest {
|
|
178
|
-
const result = exec.spawnSync("tar", ["-xOf", path, "package/package.json"], {
|
|
179
|
-
cwd: process.cwd(),
|
|
180
|
-
stdout: "capture",
|
|
181
|
-
stderr: "capture",
|
|
182
|
-
stdin: "ignore",
|
|
183
|
-
check: false,
|
|
184
|
-
});
|
|
185
|
-
if (result.exitCode !== 0 || !result.stdout) {
|
|
186
|
-
throw new Error(`Cannot read npm package manifest from ${path}: ${result.stderr}`);
|
|
187
|
-
}
|
|
188
|
-
return JSON.parse(result.stdout) as NpmArchiveManifest;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Read and hash the release identity embedded in an npm archive. */
|
|
192
|
-
export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
193
|
-
const manifest = readNpmArchiveManifest(path);
|
|
194
|
-
if (!manifest.name || !manifest.version) {
|
|
195
|
-
throw new Error(`npm archive has no package name or version: ${path}`);
|
|
196
|
-
}
|
|
197
|
-
const access = manifest.publishConfig?.access;
|
|
198
|
-
if (access !== undefined && access !== "public" && access !== "restricted") {
|
|
199
|
-
throw new Error(`npm archive has invalid publishConfig.access: ${String(access)}`);
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
...(access ? { access } : {}),
|
|
203
|
-
contentDigest: npmArchiveContentDigest(path),
|
|
204
|
-
integrity: `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`,
|
|
205
|
-
name: manifest.name,
|
|
206
|
-
repository: manifest.repository,
|
|
207
|
-
version: manifest.version,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Group npm archives into dependency-ordered layers that can publish concurrently. */
|
|
212
20
|
function npmArchiveLayers(archives: readonly string[]): string[][] {
|
|
213
21
|
const byName = new Map(
|
|
214
22
|
archives.map((archive) => {
|
|
@@ -241,9 +49,7 @@ function npmArchiveLayers(archives: readonly string[]): string[][] {
|
|
|
241
49
|
);
|
|
242
50
|
}
|
|
243
51
|
layers.push(ready.map((name) => byName.get(name)!.archive));
|
|
244
|
-
for (const name of ready)
|
|
245
|
-
remaining.delete(name);
|
|
246
|
-
}
|
|
52
|
+
for (const name of ready) remaining.delete(name);
|
|
247
53
|
for (const dependencies of remaining.values()) {
|
|
248
54
|
for (const name of ready) dependencies.delete(name);
|
|
249
55
|
}
|
|
@@ -251,118 +57,6 @@ function npmArchiveLayers(archives: readonly string[]): string[][] {
|
|
|
251
57
|
return layers;
|
|
252
58
|
}
|
|
253
59
|
|
|
254
|
-
/** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
|
|
255
|
-
export function npmArchiveContentDigest(path: string): string {
|
|
256
|
-
const temp = mkdtempSync(join(tmpdir(), "projen-npm-content-"));
|
|
257
|
-
try {
|
|
258
|
-
exec.spawnSync("tar", ["-xzf", path, "-C", temp], {
|
|
259
|
-
cwd: process.cwd(),
|
|
260
|
-
stdout: "ignore",
|
|
261
|
-
stderr: "capture",
|
|
262
|
-
stdin: "ignore",
|
|
263
|
-
check: true,
|
|
264
|
-
});
|
|
265
|
-
const hash = createHash("sha512");
|
|
266
|
-
const visit = (directory: string): void => {
|
|
267
|
-
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
|
|
268
|
-
a.name.localeCompare(b.name),
|
|
269
|
-
)) {
|
|
270
|
-
const entryPath = join(directory, entry.name);
|
|
271
|
-
const name = relative(temp, entryPath).split("\\").join("/");
|
|
272
|
-
const stat = lstatSync(entryPath);
|
|
273
|
-
hash.update(`${entry.isDirectory() ? "D" : entry.isSymbolicLink() ? "L" : "F"}\0`);
|
|
274
|
-
hash.update(`${name}\0${stat.mode & 0o111}\0`);
|
|
275
|
-
if (entry.isDirectory()) visit(entryPath);
|
|
276
|
-
else if (entry.isSymbolicLink()) hash.update(readlinkSync(entryPath));
|
|
277
|
-
else hash.update(readFileSync(entryPath));
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
visit(temp);
|
|
281
|
-
return `sha512-${hash.digest("base64")}`;
|
|
282
|
-
} finally {
|
|
283
|
-
rmSync(temp, { recursive: true, force: true });
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Pack one workspace package and optionally transform only its archived manifest. */
|
|
288
|
-
export function packNpmPackage(
|
|
289
|
-
directory: string,
|
|
290
|
-
destination: string,
|
|
291
|
-
path = process.env.PATH ?? "",
|
|
292
|
-
transformManifest?: NpmArchiveManifestTransform,
|
|
293
|
-
): string {
|
|
294
|
-
const executable = process.versions.bun ? process.execPath : "bun";
|
|
295
|
-
const manifest = join(directory, "package.json");
|
|
296
|
-
const originalManifest = readFileSync(manifest, "utf8");
|
|
297
|
-
const workspaceRoot = projectUtils.root(directory) ?? directory;
|
|
298
|
-
const workspaceManifest = JSON.parse(
|
|
299
|
-
readFileSync(join(workspaceRoot, "package.json"), "utf8"),
|
|
300
|
-
) as Record<string, unknown>;
|
|
301
|
-
const packedManifest = `${JSON.stringify(
|
|
302
|
-
materializeWorkspaceManifest(
|
|
303
|
-
JSON.parse(originalManifest) as Record<string, unknown>,
|
|
304
|
-
workspaceManifest,
|
|
305
|
-
),
|
|
306
|
-
null,
|
|
307
|
-
2,
|
|
308
|
-
)}\n`;
|
|
309
|
-
const manifestMode = lstatSync(manifest).mode & 0o777;
|
|
310
|
-
const restoreManifestMode = (manifestMode & 0o200) === 0;
|
|
311
|
-
const existingArchives = new Set(readdirSync(destination));
|
|
312
|
-
if (restoreManifestMode) chmodSync(manifest, manifestMode | 0o200);
|
|
313
|
-
try {
|
|
314
|
-
writeFileSync(manifest, packedManifest);
|
|
315
|
-
exec.spawnSync(
|
|
316
|
-
executable,
|
|
317
|
-
["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
|
|
318
|
-
{
|
|
319
|
-
cwd: directory,
|
|
320
|
-
env: { ...process.env, PATH: path },
|
|
321
|
-
stdout: "inherit",
|
|
322
|
-
stderr: "inherit",
|
|
323
|
-
stdin: "ignore",
|
|
324
|
-
check: true,
|
|
325
|
-
},
|
|
326
|
-
);
|
|
327
|
-
} finally {
|
|
328
|
-
writeFileSync(manifest, originalManifest);
|
|
329
|
-
if (restoreManifestMode) chmodSync(manifest, manifestMode);
|
|
330
|
-
}
|
|
331
|
-
const archives = readdirSync(destination).filter(
|
|
332
|
-
(file) => file.endsWith(".tgz") && !existingArchives.has(file),
|
|
333
|
-
);
|
|
334
|
-
if (archives.length !== 1) {
|
|
335
|
-
throw new Error(`Expected one packed npm archive, found ${archives.length}`);
|
|
336
|
-
}
|
|
337
|
-
const archive = join(destination, archives[0]);
|
|
338
|
-
if (!transformManifest) return archive;
|
|
339
|
-
|
|
340
|
-
const temp = mkdtempSync(join(tmpdir(), "projen-npm-project-"));
|
|
341
|
-
try {
|
|
342
|
-
exec.spawnSync("tar", ["-xzf", archive, "-C", temp], {
|
|
343
|
-
cwd: process.cwd(),
|
|
344
|
-
stdout: "ignore",
|
|
345
|
-
stderr: "inherit",
|
|
346
|
-
stdin: "ignore",
|
|
347
|
-
check: true,
|
|
348
|
-
});
|
|
349
|
-
const manifestPath = join(temp, "package", "package.json");
|
|
350
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NpmArchiveManifest;
|
|
351
|
-
writeFileSync(manifestPath, `${JSON.stringify(transformManifest(manifest), null, 2)}\n`);
|
|
352
|
-
rmSync(archive, { force: true });
|
|
353
|
-
exec.spawnSync("tar", ["-czf", archive, "-C", temp, ...archiveFileEntries(temp)], {
|
|
354
|
-
cwd: process.cwd(),
|
|
355
|
-
stdout: "ignore",
|
|
356
|
-
stderr: "inherit",
|
|
357
|
-
stdin: "ignore",
|
|
358
|
-
check: true,
|
|
359
|
-
});
|
|
360
|
-
return archive;
|
|
361
|
-
} finally {
|
|
362
|
-
rmSync(temp, { recursive: true, force: true });
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
60
|
/** Publish dependency-ordered npm archives, skipping only exact registry matches. */
|
|
367
61
|
export async function publishNpmArchives(options: {
|
|
368
62
|
readonly concurrency?: number;
|
|
@@ -381,7 +75,7 @@ export async function publishNpmArchives(options: {
|
|
|
381
75
|
if (layers.length === 0) throw new Error(`No npm archives found in ${directory}`);
|
|
382
76
|
|
|
383
77
|
for (const archives of layers) {
|
|
384
|
-
await
|
|
78
|
+
await mapConcurrent(
|
|
385
79
|
archives,
|
|
386
80
|
async (archive) => {
|
|
387
81
|
const local = readNpmArchiveIdentity(archive);
|
package/tasks/publish-python.ts
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
|
-
/**
|
|
3
|
-
* Build selected uv workspace packages from isolated publication copies and
|
|
4
|
-
* optionally publish the resulting distributions.
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
chmodSync,
|
|
8
|
-
cpSync,
|
|
9
|
-
existsSync,
|
|
10
|
-
mkdirSync,
|
|
11
|
-
mkdtempSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
readdirSync,
|
|
14
|
-
rmSync,
|
|
15
|
-
writeFileSync,
|
|
16
|
-
} from "node:fs";
|
|
17
|
-
import { tmpdir } from "node:os";
|
|
18
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
19
|
-
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
20
|
-
import { parse, stringify } from "smol-toml";
|
|
2
|
+
/** Build or publish selected Python workspace distributions. */
|
|
21
3
|
import { z } from "zod";
|
|
22
4
|
import { runTaskMain, taskCommand, taskOptions, taskPositionals } from "./cli.ts";
|
|
23
5
|
import {
|
|
@@ -25,167 +7,7 @@ import {
|
|
|
25
7
|
TaskDryRunOptionSchema,
|
|
26
8
|
TaskOutputOptionSchema,
|
|
27
9
|
} from "./options.ts";
|
|
28
|
-
import {
|
|
29
|
-
import { runTaskCommand } from "../src/_task-command.ts";
|
|
30
|
-
|
|
31
|
-
interface PythonProjectFile {
|
|
32
|
-
readonly directory: string;
|
|
33
|
-
readonly name: string;
|
|
34
|
-
readonly version: string;
|
|
35
|
-
readonly path: string;
|
|
36
|
-
readonly private: boolean;
|
|
37
|
-
readonly source: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Python wheel and source archives accepted by Twine and package indexes. */
|
|
41
|
-
export function pythonDistributionPaths(directory: string): string[] {
|
|
42
|
-
return readdirSync(directory)
|
|
43
|
-
.filter((file) => file.endsWith(".whl") || file.endsWith(".tar.gz"))
|
|
44
|
-
.sort()
|
|
45
|
-
.map((file) => join(directory, file));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function removeTemporaryTree(root: string): void {
|
|
49
|
-
if (!existsSync(root)) return;
|
|
50
|
-
const makeWritable = (directory: string): void => {
|
|
51
|
-
chmodSync(directory, 0o700);
|
|
52
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
53
|
-
if (entry.isDirectory()) makeWritable(join(directory, entry.name));
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
makeWritable(root);
|
|
57
|
-
rmSync(root, { recursive: true, force: true });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function pythonProjects(root: string): PythonProjectFile[] {
|
|
61
|
-
const packageFiles = readdirSync(root, { withFileTypes: true })
|
|
62
|
-
.filter((entry) => entry.isDirectory())
|
|
63
|
-
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
64
|
-
.filter(existsSync)
|
|
65
|
-
.sort();
|
|
66
|
-
const allProjects: PythonProjectFile[] = packageFiles.map((path) => {
|
|
67
|
-
const source = readFileSync(path, "utf8");
|
|
68
|
-
const info = pythonProjectInfo(source, { parse });
|
|
69
|
-
return {
|
|
70
|
-
directory: basename(resolve(path, "..")),
|
|
71
|
-
name: info.name,
|
|
72
|
-
version: info.version,
|
|
73
|
-
path,
|
|
74
|
-
private: info.private,
|
|
75
|
-
source,
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
return allProjects;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Build and upload every selected public Python workspace distribution. */
|
|
82
|
-
export function publishPythonProjects(options: {
|
|
83
|
-
readonly dryRun?: boolean;
|
|
84
|
-
readonly indexUrl: string;
|
|
85
|
-
readonly publishUrl: string;
|
|
86
|
-
readonly root: string;
|
|
87
|
-
readonly version: string;
|
|
88
|
-
}): void {
|
|
89
|
-
const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
|
|
90
|
-
try {
|
|
91
|
-
buildPythonProjectsInternal({ ...options, output }, true);
|
|
92
|
-
runTaskCommand(
|
|
93
|
-
process.cwd(),
|
|
94
|
-
"uvx",
|
|
95
|
-
[
|
|
96
|
-
"--from",
|
|
97
|
-
"devpi-client",
|
|
98
|
-
"devpi",
|
|
99
|
-
"upload",
|
|
100
|
-
"--index",
|
|
101
|
-
options.publishUrl,
|
|
102
|
-
"--from-dir",
|
|
103
|
-
...(options.dryRun ? ["--dry-run"] : []),
|
|
104
|
-
output,
|
|
105
|
-
],
|
|
106
|
-
{ env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl } },
|
|
107
|
-
);
|
|
108
|
-
} finally {
|
|
109
|
-
removeTemporaryTree(output);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Build the exact Python release distributions without publishing them. */
|
|
114
|
-
export function buildPythonProjects(options: {
|
|
115
|
-
readonly allowEmpty?: boolean;
|
|
116
|
-
readonly output: string;
|
|
117
|
-
readonly packages?: readonly string[];
|
|
118
|
-
/** Place each package's artifacts in its directory beneath output. */
|
|
119
|
-
readonly packageDirectories?: boolean;
|
|
120
|
-
readonly root: string;
|
|
121
|
-
readonly version: string;
|
|
122
|
-
}): void {
|
|
123
|
-
buildPythonProjectsInternal(options, false);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function buildPythonProjectsInternal(
|
|
127
|
-
options: Parameters<typeof buildPythonProjects>[0],
|
|
128
|
-
allowOutputOutsideWorkspace: boolean,
|
|
129
|
-
): void {
|
|
130
|
-
const root = resolve(options.root);
|
|
131
|
-
const output = resolve(options.output);
|
|
132
|
-
const workspace = projectUtils.root(root) ?? dirname(root);
|
|
133
|
-
const outputPath = relative(workspace, output);
|
|
134
|
-
if (
|
|
135
|
-
!allowOutputOutsideWorkspace &&
|
|
136
|
-
(!outputPath || outputPath.startsWith("..") || isAbsolute(outputPath))
|
|
137
|
-
) {
|
|
138
|
-
throw new Error("Python release output must be a non-root directory inside the workspace");
|
|
139
|
-
}
|
|
140
|
-
rmSync(output, { recursive: true, force: true });
|
|
141
|
-
const allProjects = pythonProjects(root);
|
|
142
|
-
const requested = new Set(options.packages ?? []);
|
|
143
|
-
const missing = [...requested].filter(
|
|
144
|
-
(directory) => !allProjects.some((project) => project.directory === directory),
|
|
145
|
-
);
|
|
146
|
-
if (missing.length > 0) throw new Error(`Unknown Python packages: ${missing.join(", ")}`);
|
|
147
|
-
const projects = allProjects.filter(
|
|
148
|
-
(project) => !project.private && (requested.size === 0 || requested.has(project.directory)),
|
|
149
|
-
);
|
|
150
|
-
if (projects.length === 0 && options.allowEmpty) {
|
|
151
|
-
mkdirSync(output, { recursive: true });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
155
|
-
|
|
156
|
-
const temporaryRoot = mkdtempSync(join(tmpdir(), "projen-python-release-"));
|
|
157
|
-
try {
|
|
158
|
-
mkdirSync(output, { recursive: true });
|
|
159
|
-
for (const project of projects) {
|
|
160
|
-
const packageRoot = join(temporaryRoot, project.directory);
|
|
161
|
-
cpSync(dirname(project.path), packageRoot, { recursive: true });
|
|
162
|
-
const manifestPath = join(packageRoot, "pyproject.toml");
|
|
163
|
-
chmodSync(manifestPath, 0o644);
|
|
164
|
-
writeFileSync(
|
|
165
|
-
manifestPath,
|
|
166
|
-
preparePythonProjectForPublication(project.source, {
|
|
167
|
-
packages: allProjects,
|
|
168
|
-
toml: { parse, stringify },
|
|
169
|
-
version: options.version,
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
172
|
-
const packageOutput = options.packageDirectories ? join(output, project.directory) : output;
|
|
173
|
-
runTaskCommand(root, "uv", ["build", "--out-dir", packageOutput, packageRoot]);
|
|
174
|
-
if (pythonDistributionPaths(packageOutput).length === 0) {
|
|
175
|
-
throw new Error(`No Python distributions found for ${project.name}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const distributions = options.packageDirectories
|
|
179
|
-
? projects.flatMap((project) => pythonDistributionPaths(join(output, project.directory)))
|
|
180
|
-
: pythonDistributionPaths(output);
|
|
181
|
-
if (distributions.length === 0) {
|
|
182
|
-
throw new Error(`No Python distributions found in ${output}`);
|
|
183
|
-
}
|
|
184
|
-
runTaskCommand(process.cwd(), "uvx", ["twine", "check", ...distributions]);
|
|
185
|
-
} finally {
|
|
186
|
-
removeTemporaryTree(temporaryRoot);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
10
|
+
import { buildPythonProjects, publishPythonProjects } from "../src/python-packaging.ts";
|
|
189
11
|
|
|
190
12
|
/** Parse command-line publication options and run the Python release task. */
|
|
191
13
|
export const PublishPythonOptionsSchema = z.object({
|
package/tasks/publish.ts
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
47
47
|
import { tmpdir } from "node:os";
|
|
48
48
|
import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
49
|
-
import {
|
|
49
|
+
import { log, mapConcurrent, sequence } from "@dbx-tools/shared-core";
|
|
50
50
|
import { z } from "zod";
|
|
51
51
|
import { runTaskMain, taskCommand, taskOptions, taskPositionals, taskRoot } from "./cli.ts";
|
|
52
52
|
import {
|
|
@@ -56,20 +56,17 @@ import {
|
|
|
56
56
|
TaskOutputOptionSchema,
|
|
57
57
|
TaskRootOptionSchema,
|
|
58
58
|
} from "./options.ts";
|
|
59
|
+
import { runTaskCommand, runTaskCommandAsync } from "../src/_task-command.ts";
|
|
60
|
+
import { applyPublishConfig, packNpmPackage } from "../src/npm-packaging.ts";
|
|
59
61
|
import {
|
|
60
|
-
applyPublishConfig,
|
|
61
62
|
npmReleaseMatches,
|
|
62
|
-
packNpmPackage,
|
|
63
63
|
publishedNpmRelease,
|
|
64
64
|
readNpmArchiveIdentity,
|
|
65
|
-
} from "
|
|
66
|
-
import { runTaskCommand, runTaskCommandAsync } from "../src/_task-command.ts";
|
|
65
|
+
} from "../src/npm-release.ts";
|
|
67
66
|
import { recordedPackages } from "../src/packages.ts";
|
|
68
67
|
|
|
69
68
|
const logger = log.logger("projen:publish");
|
|
70
69
|
|
|
71
|
-
export { applyPublishConfig };
|
|
72
|
-
|
|
73
70
|
/** Whether selected workspace members carry the reviewed workspace version. */
|
|
74
71
|
function manifestsMatchVersion(members: readonly string[], version: string): boolean {
|
|
75
72
|
return members.every((dir) => {
|
|
@@ -187,9 +184,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
187
184
|
`${pkg.name ?? dirname(dir)} has invalid publishConfig.access ${String(access)}`,
|
|
188
185
|
);
|
|
189
186
|
}
|
|
190
|
-
const compiledTargets = [
|
|
191
|
-
...object.sequence(compiledPublishTargets(pkg.publishConfig)).distinct(),
|
|
192
|
-
];
|
|
187
|
+
const compiledTargets = [...sequence(compiledPublishTargets(pkg.publishConfig)).distinct()];
|
|
193
188
|
publishable.push({
|
|
194
189
|
dir,
|
|
195
190
|
name: pkg.name ?? dirname(dir),
|
|
@@ -232,7 +227,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
232
227
|
rmSync(output, { recursive: true, force: true });
|
|
233
228
|
mkdirSync(output, { recursive: true });
|
|
234
229
|
}
|
|
235
|
-
await
|
|
230
|
+
await mapConcurrent(
|
|
236
231
|
publishable,
|
|
237
232
|
async ({ dir, name, version: packageVersion, access }) => {
|
|
238
233
|
const packed = mkdtempSync(join(tmpdir(), "projen-npm-release-"));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join, relative, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
-
import { log,
|
|
9
|
+
import { log, toIdentifierWithOptions } from "@dbx-tools/shared-core";
|
|
10
10
|
import type { BunPlugin } from "bun";
|
|
11
11
|
import stdLibBrowser from "node-stdlib-browser";
|
|
12
12
|
import ts from "typescript";
|
|
@@ -949,7 +949,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
949
949
|
function pythonObjectName(type: ts.Type, path: string, suffix: string): string {
|
|
950
950
|
const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
|
|
951
951
|
if (name && name !== "__type") return name;
|
|
952
|
-
const identifier =
|
|
952
|
+
const identifier = toIdentifierWithOptions({ delimiter: "_" }, path);
|
|
953
953
|
return `${identifier
|
|
954
954
|
.split("_")
|
|
955
955
|
.filter(Boolean)
|
package/tasks/python-registry.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { isLoopbackHost, parseList } from "@dbx-tools/shared-core";
|
|
6
6
|
import { parse } from "smol-toml";
|
|
7
7
|
import { captureTaskCommand } from "../src/_task-command.ts";
|
|
8
8
|
|
|
@@ -97,7 +97,7 @@ export function devpiRegistry(index: string): LocalPythonRegistry | undefined {
|
|
|
97
97
|
} catch {
|
|
98
98
|
return undefined;
|
|
99
99
|
}
|
|
100
|
-
if (!
|
|
100
|
+
if (!isLoopbackHost(url)) return undefined;
|
|
101
101
|
|
|
102
102
|
// A globally-configured publish-url wins: it names the deploy target directly,
|
|
103
103
|
// so we honor it even if the index URL isn't the conventional `+simple` shape.
|
|
@@ -147,8 +147,8 @@ export function activePythonIndexes(): string[] {
|
|
|
147
147
|
// uv: env vars first (primary, then extras), then uv.toml's index blocks.
|
|
148
148
|
add(process.env.UV_DEFAULT_INDEX);
|
|
149
149
|
add(process.env.UV_INDEX_URL);
|
|
150
|
-
for (const url of
|
|
151
|
-
for (const url of
|
|
150
|
+
for (const url of parseList(process.env.UV_INDEX)) add(url);
|
|
151
|
+
for (const url of parseList(process.env.UV_EXTRA_INDEX_URL)) add(url);
|
|
152
152
|
const uvConfig = process.env.UV_CONFIG_FILE ?? resolve(homedir(), ".config/uv/uv.toml");
|
|
153
153
|
if (existsSync(uvConfig)) {
|
|
154
154
|
for (const url of parseUvIndexes(readFileSync(uvConfig, "utf8"))) add(url);
|
|
@@ -156,9 +156,9 @@ export function activePythonIndexes(): string[] {
|
|
|
156
156
|
|
|
157
157
|
// pip: env vars (primary + extras), then `pip config` (index-url + extra-index-url).
|
|
158
158
|
add(process.env.PIP_INDEX_URL);
|
|
159
|
-
for (const url of
|
|
159
|
+
for (const url of parseList(process.env.PIP_EXTRA_INDEX_URL)) add(url);
|
|
160
160
|
add(pipConfig("global.index-url"));
|
|
161
|
-
for (const url of
|
|
161
|
+
for (const url of parseList(pipConfig("global.extra-index-url"))) add(url);
|
|
162
162
|
|
|
163
163
|
return out;
|
|
164
164
|
}
|
package/tasks/python-sync.ts
CHANGED
|
@@ -20,8 +20,7 @@ import {
|
|
|
20
20
|
} from "node:fs";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
22
22
|
import { basename, dirname, join, resolve } from "node:path";
|
|
23
|
-
import
|
|
24
|
-
import { withFileLock } from "@dbx-tools/core/file-lock";
|
|
23
|
+
import { spawnSync, withFileLock } from "@dbx-tools/core";
|
|
25
24
|
import { parse } from "smol-toml";
|
|
26
25
|
import { z } from "zod";
|
|
27
26
|
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
@@ -253,7 +252,7 @@ function localizeImports(
|
|
|
253
252
|
? segments.join(".")
|
|
254
253
|
: "";
|
|
255
254
|
// Captured stderr carries the localizer's reason into the thrown error.
|
|
256
|
-
const { stdout } =
|
|
255
|
+
const { stdout } = spawnSync(
|
|
257
256
|
"uv",
|
|
258
257
|
[
|
|
259
258
|
"run",
|