@dbx-tools/projen 0.9.32 → 0.9.37
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 +71 -65
- package/index.ts +5 -8
- package/package.json +8 -7
- package/src/_task-command.ts +0 -75
- package/src/barrels.ts +15 -23
- package/src/bun-app.ts +5 -6
- package/src/clean.ts +41 -15
- package/src/codegen.ts +2 -3
- package/src/dbx-tools-config.ts +23 -64
- package/src/generated.ts +7 -1
- package/src/module-exports.ts +35 -3
- package/src/openapi.ts +34 -38
- package/src/packages.ts +3 -30
- package/src/pnpm-workspace.ts +21 -26
- package/src/project-js.ts +152 -365
- package/src/project-predicate.ts +4 -5
- package/src/project-py.ts +55 -144
- package/src/publish.ts +1 -1
- package/src/python-node-bindings.ts +140 -67
- package/src/python-node-bundle.ts +6 -42
- package/src/release-context.ts +4 -4
- package/src/release.ts +47 -85
- package/src/tags.ts +20 -18
- package/src/tsconfig.ts +9 -5
- package/src/vscode.ts +47 -58
- package/src/watch.ts +2 -8
- package/src/workspace-version.ts +1 -6
- package/tasks/barrels.ts +43 -38
- package/tasks/bump.ts +24 -20
- package/tasks/clean.ts +54 -50
- package/tasks/compile-workspace.ts +2 -117
- package/tasks/lib/compile-workspace.ts +119 -0
- package/tasks/lib/local-publish.ts +122 -0
- package/tasks/lib/publish-npm.ts +458 -0
- package/tasks/lib/publish-python.ts +195 -0
- package/tasks/lib/publish.ts +264 -0
- package/tasks/lib/python-node-bindings.ts +1262 -0
- package/tasks/{python-registry.ts → lib/python-registry.ts} +21 -30
- package/tasks/{python-release.ts → lib/python-release.ts} +1 -0
- package/tasks/lib/release-version.ts +66 -0
- package/tasks/lib/release.ts +212 -0
- package/tasks/local-publish.ts +4 -118
- package/tasks/openapi.ts +25 -21
- package/tasks/projenrc.ts +14 -12
- package/tasks/publish-npm.ts +3 -364
- package/tasks/publish-python.ts +3 -184
- package/tasks/publish.ts +3 -279
- package/tasks/python-node-bindings.ts +3 -1014
- package/tasks/release-version.ts +3 -62
- package/tasks/release.ts +3 -121
- package/tasks/sync.ts +71 -70
- package/tasks/version-check.ts +3 -10
- package/src/release-catalog.ts +0 -684
- package/tasks/databricks-cli-assets.ts +0 -129
- package/tasks/python-node-bindings-watch.ts +0 -47
package/tasks/publish-npm.ts
CHANGED
|
@@ -1,367 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
|
-
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
chmodSync,
|
|
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 { log } from "@dbx-tools/shared-core";
|
|
18
|
-
import { Command } from "commander";
|
|
19
|
-
import { runTaskCommand } from "../src/_task-command.ts";
|
|
2
|
+
import { main } from "./lib/publish-npm.ts";
|
|
20
3
|
|
|
21
|
-
|
|
22
|
-
const logger = log.logger("projen:publish-npm");
|
|
4
|
+
export * from "./lib/publish-npm.ts";
|
|
23
5
|
|
|
24
|
-
|
|
25
|
-
readonly access?: "public" | "restricted";
|
|
26
|
-
readonly contentDigest?: string;
|
|
27
|
-
readonly integrity?: string;
|
|
28
|
-
readonly name: string;
|
|
29
|
-
readonly repository?: unknown;
|
|
30
|
-
readonly version: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface NpmArchiveManifest {
|
|
34
|
-
readonly [key: string]: unknown;
|
|
35
|
-
readonly dependencies?: Readonly<Record<string, string>>;
|
|
36
|
-
readonly optionalDependencies?: Readonly<Record<string, string>>;
|
|
37
|
-
readonly name?: string;
|
|
38
|
-
readonly publishConfig?: { readonly access?: unknown };
|
|
39
|
-
readonly repository?: unknown;
|
|
40
|
-
readonly version?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type NpmArchiveManifestTransform = (
|
|
44
|
-
manifest: NpmArchiveManifest,
|
|
45
|
-
) => Readonly<Record<string, unknown>>;
|
|
46
|
-
|
|
47
|
-
function archiveFileEntries(directory: string, prefix = ""): string[] {
|
|
48
|
-
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
49
|
-
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
50
|
-
return entry.isDirectory()
|
|
51
|
-
? archiveFileEntries(join(directory, entry.name), relativePath)
|
|
52
|
-
: [relativePath];
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizedRepository(value: unknown): string | undefined {
|
|
57
|
-
const url =
|
|
58
|
-
typeof value === "string"
|
|
59
|
-
? value
|
|
60
|
-
: value && typeof value === "object" && "url" in value
|
|
61
|
-
? String(value.url)
|
|
62
|
-
: undefined;
|
|
63
|
-
return url
|
|
64
|
-
?.replace(/^git\+/, "")
|
|
65
|
-
.replace(/\.git$/, "")
|
|
66
|
-
.replace(/\/$/, "");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function registryUrl(registry: string, name: string, version: string): string {
|
|
70
|
-
return `${registry.replace(/\/$/, "")}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function npmReleaseMatches(
|
|
74
|
-
local: NpmReleaseIdentity,
|
|
75
|
-
published: NpmReleaseIdentity | undefined,
|
|
76
|
-
): boolean {
|
|
77
|
-
if (!published) return false;
|
|
78
|
-
if (published.name !== local.name || published.version !== local.version) {
|
|
79
|
-
throw new Error(`Published npm identity does not match ${local.name}@${local.version}`);
|
|
80
|
-
}
|
|
81
|
-
const localRepository = normalizedRepository(local.repository);
|
|
82
|
-
const publishedRepository = normalizedRepository(published.repository);
|
|
83
|
-
if (localRepository && publishedRepository && localRepository !== publishedRepository) {
|
|
84
|
-
throw new Error(`Published npm repository does not match ${local.name}@${local.version}`);
|
|
85
|
-
}
|
|
86
|
-
if (
|
|
87
|
-
local.contentDigest &&
|
|
88
|
-
published.contentDigest &&
|
|
89
|
-
local.contentDigest !== published.contentDigest
|
|
90
|
-
) {
|
|
91
|
-
throw new Error(`Published npm content does not match ${local.name}@${local.version}`);
|
|
92
|
-
}
|
|
93
|
-
if (
|
|
94
|
-
!(local.contentDigest && published.contentDigest) &&
|
|
95
|
-
local.integrity &&
|
|
96
|
-
published.integrity !== local.integrity
|
|
97
|
-
) {
|
|
98
|
-
throw new Error(`Published npm integrity does not match ${local.name}@${local.version}`);
|
|
99
|
-
}
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export async function publishedNpmRelease(
|
|
104
|
-
name: string,
|
|
105
|
-
version: string,
|
|
106
|
-
registry = process.env.NPM_CONFIG_REGISTRY ?? DEFAULT_REGISTRY,
|
|
107
|
-
): Promise<NpmReleaseIdentity | undefined> {
|
|
108
|
-
const response = await fetch(registryUrl(registry, name, version), {
|
|
109
|
-
headers: { accept: "application/json" },
|
|
110
|
-
});
|
|
111
|
-
if (response.status === 404) return undefined;
|
|
112
|
-
if (!response.ok) {
|
|
113
|
-
throw new Error(`npm registry lookup failed for ${name}@${version}: ${response.status}`);
|
|
114
|
-
}
|
|
115
|
-
const metadata = (await response.json()) as {
|
|
116
|
-
dist?: { integrity?: string; tarball?: string };
|
|
117
|
-
name?: string;
|
|
118
|
-
repository?: unknown;
|
|
119
|
-
version?: string;
|
|
120
|
-
};
|
|
121
|
-
if (!metadata.name || !metadata.version) {
|
|
122
|
-
throw new Error(`npm registry returned an incomplete identity for ${name}@${version}`);
|
|
123
|
-
}
|
|
124
|
-
let contentDigest: string | undefined;
|
|
125
|
-
if (metadata.dist?.tarball) {
|
|
126
|
-
const archive = await fetch(metadata.dist.tarball);
|
|
127
|
-
if (!archive.ok) {
|
|
128
|
-
throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
|
|
129
|
-
}
|
|
130
|
-
const temp = mkdtempSync(join(tmpdir(), "projen-published-npm-"));
|
|
131
|
-
const path = join(temp, "package.tgz");
|
|
132
|
-
try {
|
|
133
|
-
writeFileSync(path, Buffer.from(await archive.arrayBuffer()));
|
|
134
|
-
contentDigest = npmArchiveContentDigest(path);
|
|
135
|
-
} finally {
|
|
136
|
-
rmSync(temp, { recursive: true, force: true });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
...(contentDigest ? { contentDigest } : {}),
|
|
141
|
-
integrity: metadata.dist?.integrity,
|
|
142
|
-
name: metadata.name,
|
|
143
|
-
repository: metadata.repository,
|
|
144
|
-
version: metadata.version,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function readNpmArchiveManifest(path: string): NpmArchiveManifest {
|
|
149
|
-
const result = exec.spawnSync("tar", ["-xOf", path, "package/package.json"], {
|
|
150
|
-
cwd: process.cwd(),
|
|
151
|
-
stdout: "capture",
|
|
152
|
-
stderr: "capture",
|
|
153
|
-
stdin: "ignore",
|
|
154
|
-
check: false,
|
|
155
|
-
});
|
|
156
|
-
if (result.exitCode !== 0 || !result.stdout) {
|
|
157
|
-
throw new Error(`Cannot read npm package manifest from ${path}: ${result.stderr}`);
|
|
158
|
-
}
|
|
159
|
-
return JSON.parse(result.stdout) as NpmArchiveManifest;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
163
|
-
const manifest = readNpmArchiveManifest(path);
|
|
164
|
-
if (!manifest.name || !manifest.version) {
|
|
165
|
-
throw new Error(`npm archive has no package name or version: ${path}`);
|
|
166
|
-
}
|
|
167
|
-
const access = manifest.publishConfig?.access;
|
|
168
|
-
if (access !== undefined && access !== "public" && access !== "restricted") {
|
|
169
|
-
throw new Error(`npm archive has invalid publishConfig.access: ${String(access)}`);
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
...(access ? { access } : {}),
|
|
173
|
-
contentDigest: npmArchiveContentDigest(path),
|
|
174
|
-
integrity: `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`,
|
|
175
|
-
name: manifest.name,
|
|
176
|
-
repository: manifest.repository,
|
|
177
|
-
version: manifest.version,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function orderNpmArchives(archives: readonly string[]): string[] {
|
|
182
|
-
const byName = new Map(
|
|
183
|
-
archives.map((archive) => {
|
|
184
|
-
const manifest = readNpmArchiveManifest(archive);
|
|
185
|
-
if (!manifest.name) throw new Error(`npm archive has no package name: ${archive}`);
|
|
186
|
-
return [manifest.name, { archive, manifest }] as const;
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
if (byName.size !== archives.length) throw new Error("Release contains duplicate npm packages");
|
|
190
|
-
const remaining = new Map(
|
|
191
|
-
[...byName].map(([name, value]) => [
|
|
192
|
-
name,
|
|
193
|
-
new Set(
|
|
194
|
-
[
|
|
195
|
-
...Object.keys(value.manifest.dependencies ?? {}),
|
|
196
|
-
...Object.keys(value.manifest.optionalDependencies ?? {}),
|
|
197
|
-
].filter((dependency) => byName.has(dependency)),
|
|
198
|
-
),
|
|
199
|
-
]),
|
|
200
|
-
);
|
|
201
|
-
const ordered: string[] = [];
|
|
202
|
-
while (remaining.size > 0) {
|
|
203
|
-
const ready = [...remaining]
|
|
204
|
-
.filter(([, dependencies]) => dependencies.size === 0)
|
|
205
|
-
.map(([name]) => name)
|
|
206
|
-
.sort();
|
|
207
|
-
if (ready.length === 0) {
|
|
208
|
-
throw new Error(
|
|
209
|
-
`Cyclic npm release dependencies: ${[...remaining.keys()].sort().join(", ")}`,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
for (const name of ready) {
|
|
213
|
-
ordered.push(byName.get(name)!.archive);
|
|
214
|
-
remaining.delete(name);
|
|
215
|
-
}
|
|
216
|
-
for (const dependencies of remaining.values()) {
|
|
217
|
-
for (const name of ready) dependencies.delete(name);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return ordered;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
|
|
224
|
-
export function npmArchiveContentDigest(path: string): string {
|
|
225
|
-
const temp = mkdtempSync(join(tmpdir(), "projen-npm-content-"));
|
|
226
|
-
try {
|
|
227
|
-
exec.spawnSync("tar", ["-xzf", path, "-C", temp], {
|
|
228
|
-
cwd: process.cwd(),
|
|
229
|
-
stdout: "ignore",
|
|
230
|
-
stderr: "capture",
|
|
231
|
-
stdin: "ignore",
|
|
232
|
-
check: true,
|
|
233
|
-
});
|
|
234
|
-
const hash = createHash("sha512");
|
|
235
|
-
const visit = (directory: string): void => {
|
|
236
|
-
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
|
|
237
|
-
a.name.localeCompare(b.name),
|
|
238
|
-
)) {
|
|
239
|
-
const entryPath = join(directory, entry.name);
|
|
240
|
-
const name = relative(temp, entryPath).split("\\").join("/");
|
|
241
|
-
const stat = lstatSync(entryPath);
|
|
242
|
-
hash.update(`${entry.isDirectory() ? "D" : entry.isSymbolicLink() ? "L" : "F"}\0`);
|
|
243
|
-
hash.update(`${name}\0${stat.mode & 0o111}\0`);
|
|
244
|
-
if (entry.isDirectory()) visit(entryPath);
|
|
245
|
-
else if (entry.isSymbolicLink()) hash.update(readlinkSync(entryPath));
|
|
246
|
-
else hash.update(readFileSync(entryPath));
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
visit(temp);
|
|
250
|
-
return `sha512-${hash.digest("base64")}`;
|
|
251
|
-
} finally {
|
|
252
|
-
rmSync(temp, { recursive: true, force: true });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function packNpmPackage(
|
|
257
|
-
directory: string,
|
|
258
|
-
destination: string,
|
|
259
|
-
path = process.env.PATH ?? "",
|
|
260
|
-
transformManifest?: NpmArchiveManifestTransform,
|
|
261
|
-
): string {
|
|
262
|
-
const executable = process.versions.bun ? process.execPath : "bun";
|
|
263
|
-
const manifest = join(directory, "package.json");
|
|
264
|
-
const manifestMode = lstatSync(manifest).mode & 0o777;
|
|
265
|
-
const restoreManifestMode = (manifestMode & 0o200) === 0;
|
|
266
|
-
if (restoreManifestMode) chmodSync(manifest, manifestMode | 0o200);
|
|
267
|
-
try {
|
|
268
|
-
exec.spawnSync(
|
|
269
|
-
executable,
|
|
270
|
-
["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
|
|
271
|
-
{
|
|
272
|
-
cwd: directory,
|
|
273
|
-
env: { ...process.env, PATH: path },
|
|
274
|
-
stdout: "inherit",
|
|
275
|
-
stderr: "inherit",
|
|
276
|
-
stdin: "ignore",
|
|
277
|
-
check: true,
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
} finally {
|
|
281
|
-
if (restoreManifestMode) chmodSync(manifest, manifestMode);
|
|
282
|
-
}
|
|
283
|
-
const archives = readdirSync(destination).filter((file) => file.endsWith(".tgz"));
|
|
284
|
-
if (archives.length !== 1) {
|
|
285
|
-
throw new Error(`Expected one packed npm archive, found ${archives.length}`);
|
|
286
|
-
}
|
|
287
|
-
const archive = join(destination, archives[0]);
|
|
288
|
-
if (!transformManifest) return archive;
|
|
289
|
-
|
|
290
|
-
const temp = mkdtempSync(join(tmpdir(), "projen-npm-project-"));
|
|
291
|
-
try {
|
|
292
|
-
exec.spawnSync("tar", ["-xzf", archive, "-C", temp], {
|
|
293
|
-
cwd: process.cwd(),
|
|
294
|
-
stdout: "ignore",
|
|
295
|
-
stderr: "inherit",
|
|
296
|
-
stdin: "ignore",
|
|
297
|
-
check: true,
|
|
298
|
-
});
|
|
299
|
-
const manifestPath = join(temp, "package", "package.json");
|
|
300
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NpmArchiveManifest;
|
|
301
|
-
writeFileSync(manifestPath, `${JSON.stringify(transformManifest(manifest), null, 2)}\n`);
|
|
302
|
-
rmSync(archive, { force: true });
|
|
303
|
-
exec.spawnSync("tar", ["-czf", archive, "-C", temp, ...archiveFileEntries(temp)], {
|
|
304
|
-
cwd: process.cwd(),
|
|
305
|
-
stdout: "ignore",
|
|
306
|
-
stderr: "inherit",
|
|
307
|
-
stdin: "ignore",
|
|
308
|
-
check: true,
|
|
309
|
-
});
|
|
310
|
-
return archive;
|
|
311
|
-
} finally {
|
|
312
|
-
rmSync(temp, { recursive: true, force: true });
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export async function publishNpmArchives(options: {
|
|
317
|
-
readonly directory: string;
|
|
318
|
-
readonly dryRun?: boolean;
|
|
319
|
-
readonly registry?: string;
|
|
320
|
-
readonly version?: string;
|
|
321
|
-
}): Promise<void> {
|
|
322
|
-
const directory = resolve(options.directory);
|
|
323
|
-
const archives = orderNpmArchives(
|
|
324
|
-
readdirSync(directory)
|
|
325
|
-
.filter((file) => file.endsWith(".tgz"))
|
|
326
|
-
.sort()
|
|
327
|
-
.map((file) => join(directory, file)),
|
|
328
|
-
);
|
|
329
|
-
if (archives.length === 0) throw new Error(`No npm archives found in ${directory}`);
|
|
330
|
-
|
|
331
|
-
for (const archive of archives) {
|
|
332
|
-
const local = readNpmArchiveIdentity(archive);
|
|
333
|
-
if (options.version && local.version !== options.version) {
|
|
334
|
-
throw new Error(
|
|
335
|
-
`npm archive ${archive} carries ${local.version}, expected ${options.version}`,
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
if (!options.dryRun) {
|
|
339
|
-
const published = await publishedNpmRelease(local.name, local.version, options.registry);
|
|
340
|
-
if (npmReleaseMatches(local, published)) {
|
|
341
|
-
logger.info(`skip published ${local.name}@${local.version}`);
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
runTaskCommand(process.cwd(), "npm", [
|
|
346
|
-
"publish",
|
|
347
|
-
archive,
|
|
348
|
-
...(local.access ? ["--access", local.access] : []),
|
|
349
|
-
...(options.registry ? ["--registry", options.registry] : []),
|
|
350
|
-
...(options.dryRun ? ["--dry-run"] : []),
|
|
351
|
-
]);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (import.meta.main) {
|
|
356
|
-
const program = new Command();
|
|
357
|
-
program
|
|
358
|
-
.requiredOption("--directory <path>", "Directory containing npm archives")
|
|
359
|
-
.option("--version <version>", "Expected npm release version")
|
|
360
|
-
.option("--registry <url>", "npm registry URL")
|
|
361
|
-
.option("--dry-run", "Validate archives without publishing")
|
|
362
|
-
.action(
|
|
363
|
-
(options: { directory: string; dryRun?: boolean; registry?: string; version?: string }) =>
|
|
364
|
-
publishNpmArchives(options),
|
|
365
|
-
);
|
|
366
|
-
await program.parseAsync();
|
|
367
|
-
}
|
|
6
|
+
if (import.meta.main) await main();
|
package/tasks/publish-python.ts
CHANGED
|
@@ -1,187 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
|
-
import {
|
|
3
|
-
chmodSync,
|
|
4
|
-
cpSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
mkdtempSync,
|
|
8
|
-
readFileSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
rmSync,
|
|
11
|
-
writeFileSync,
|
|
12
|
-
} from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
15
|
-
import { Command } from "commander";
|
|
16
|
-
import { parse, stringify } from "smol-toml";
|
|
17
|
-
import { preparePythonProjectForPublication, pythonProjectInfo } from "./python-release.ts";
|
|
18
|
-
import { runTaskCommand } from "../src/_task-command.ts";
|
|
2
|
+
import { main } from "./lib/publish-python.ts";
|
|
19
3
|
|
|
20
|
-
|
|
21
|
-
readonly directory: string;
|
|
22
|
-
readonly name: string;
|
|
23
|
-
readonly version: string;
|
|
24
|
-
readonly path: string;
|
|
25
|
-
readonly private: boolean;
|
|
26
|
-
readonly source: string;
|
|
27
|
-
}
|
|
4
|
+
export * from "./lib/publish-python.ts";
|
|
28
5
|
|
|
29
|
-
|
|
30
|
-
export function pythonDistributionPaths(directory: string): string[] {
|
|
31
|
-
return readdirSync(directory)
|
|
32
|
-
.filter((file) => file.endsWith(".whl") || file.endsWith(".tar.gz"))
|
|
33
|
-
.sort()
|
|
34
|
-
.map((file) => join(directory, file));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function pythonProjects(root: string): PythonProjectFile[] {
|
|
38
|
-
const packageFiles = readdirSync(root, { withFileTypes: true })
|
|
39
|
-
.filter((entry) => entry.isDirectory())
|
|
40
|
-
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
41
|
-
.filter(existsSync)
|
|
42
|
-
.sort();
|
|
43
|
-
const allProjects: PythonProjectFile[] = packageFiles.map((path) => {
|
|
44
|
-
const source = readFileSync(path, "utf8");
|
|
45
|
-
const info = pythonProjectInfo(source, { parse });
|
|
46
|
-
return {
|
|
47
|
-
directory: basename(resolve(path, "..")),
|
|
48
|
-
name: info.name,
|
|
49
|
-
version: info.version,
|
|
50
|
-
path,
|
|
51
|
-
private: info.private,
|
|
52
|
-
source,
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
return allProjects;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function publishPythonProjects(options: {
|
|
59
|
-
readonly dryRun?: boolean;
|
|
60
|
-
readonly indexUrl: string;
|
|
61
|
-
readonly publishUrl: string;
|
|
62
|
-
readonly root: string;
|
|
63
|
-
readonly version: string;
|
|
64
|
-
}): void {
|
|
65
|
-
const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
|
|
66
|
-
try {
|
|
67
|
-
buildPythonProjects({ ...options, output });
|
|
68
|
-
runTaskCommand(
|
|
69
|
-
process.cwd(),
|
|
70
|
-
"uvx",
|
|
71
|
-
[
|
|
72
|
-
"--from",
|
|
73
|
-
"devpi-client",
|
|
74
|
-
"devpi",
|
|
75
|
-
"upload",
|
|
76
|
-
"--index",
|
|
77
|
-
options.publishUrl,
|
|
78
|
-
"--from-dir",
|
|
79
|
-
...(options.dryRun ? ["--dry-run"] : []),
|
|
80
|
-
output,
|
|
81
|
-
],
|
|
82
|
-
{ env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl } },
|
|
83
|
-
);
|
|
84
|
-
} finally {
|
|
85
|
-
rmSync(output, { recursive: true, force: true });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Build the exact Python release distributions without publishing them. */
|
|
90
|
-
export function buildPythonProjects(options: {
|
|
91
|
-
readonly allowEmpty?: boolean;
|
|
92
|
-
readonly output: string;
|
|
93
|
-
readonly packages?: readonly string[];
|
|
94
|
-
readonly root: string;
|
|
95
|
-
readonly version: string;
|
|
96
|
-
}): void {
|
|
97
|
-
const root = resolve(options.root);
|
|
98
|
-
const output = resolve(options.output);
|
|
99
|
-
rmSync(output, { recursive: true, force: true });
|
|
100
|
-
const allProjects = pythonProjects(root);
|
|
101
|
-
const requested = new Set(options.packages ?? []);
|
|
102
|
-
const missing = [...requested].filter(
|
|
103
|
-
(directory) => !allProjects.some((project) => project.directory === directory),
|
|
104
|
-
);
|
|
105
|
-
if (missing.length > 0) throw new Error(`Unknown Python packages: ${missing.join(", ")}`);
|
|
106
|
-
const projects = allProjects.filter(
|
|
107
|
-
(project) => !project.private && (requested.size === 0 || requested.has(project.directory)),
|
|
108
|
-
);
|
|
109
|
-
if (projects.length === 0 && options.allowEmpty) {
|
|
110
|
-
mkdirSync(output, { recursive: true });
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
114
|
-
|
|
115
|
-
const temporaryRoot = mkdtempSync(join(tmpdir(), "projen-python-release-"));
|
|
116
|
-
try {
|
|
117
|
-
mkdirSync(output, { recursive: true });
|
|
118
|
-
for (const project of projects) {
|
|
119
|
-
const packageRoot = join(temporaryRoot, project.directory);
|
|
120
|
-
cpSync(dirname(project.path), packageRoot, { recursive: true });
|
|
121
|
-
const manifestPath = join(packageRoot, "pyproject.toml");
|
|
122
|
-
chmodSync(manifestPath, 0o644);
|
|
123
|
-
writeFileSync(
|
|
124
|
-
manifestPath,
|
|
125
|
-
preparePythonProjectForPublication(project.source, {
|
|
126
|
-
packages: allProjects,
|
|
127
|
-
toml: { parse, stringify },
|
|
128
|
-
version: options.version,
|
|
129
|
-
}),
|
|
130
|
-
);
|
|
131
|
-
runTaskCommand(root, "uv", ["build", "--out-dir", output, packageRoot]);
|
|
132
|
-
}
|
|
133
|
-
const distributions = pythonDistributionPaths(output);
|
|
134
|
-
if (distributions.length === 0) {
|
|
135
|
-
throw new Error(`No Python distributions found in ${output}`);
|
|
136
|
-
}
|
|
137
|
-
runTaskCommand(process.cwd(), "uvx", ["twine", "check", ...distributions]);
|
|
138
|
-
} finally {
|
|
139
|
-
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (import.meta.main) {
|
|
144
|
-
const program = new Command();
|
|
145
|
-
program
|
|
146
|
-
.argument("<version>", "Python package version")
|
|
147
|
-
.option("--index-url <url>", "devpi Simple API URL")
|
|
148
|
-
.option("--publish-url <url>", "devpi writable index URL")
|
|
149
|
-
.option("--root <path>", "Python workspace package root", "packages/py")
|
|
150
|
-
.option("--package <directory...>", "Build only selected package directories")
|
|
151
|
-
.option("--output <path>", "Build distributions into a directory without publishing")
|
|
152
|
-
.option("--dry-run", "build and inspect distributions without uploading")
|
|
153
|
-
.action(
|
|
154
|
-
(
|
|
155
|
-
version: string,
|
|
156
|
-
options: {
|
|
157
|
-
dryRun?: boolean;
|
|
158
|
-
indexUrl?: string;
|
|
159
|
-
package?: string[];
|
|
160
|
-
publishUrl?: string;
|
|
161
|
-
root: string;
|
|
162
|
-
output?: string;
|
|
163
|
-
},
|
|
164
|
-
) => {
|
|
165
|
-
if (options.output) {
|
|
166
|
-
buildPythonProjects({
|
|
167
|
-
output: options.output,
|
|
168
|
-
packages: options.package,
|
|
169
|
-
root: options.root,
|
|
170
|
-
version,
|
|
171
|
-
});
|
|
172
|
-
} else {
|
|
173
|
-
if (!options.indexUrl || !options.publishUrl) {
|
|
174
|
-
throw new Error("--index-url and --publish-url are required when publishing");
|
|
175
|
-
}
|
|
176
|
-
publishPythonProjects({
|
|
177
|
-
dryRun: options.dryRun,
|
|
178
|
-
indexUrl: options.indexUrl,
|
|
179
|
-
publishUrl: options.publishUrl,
|
|
180
|
-
root: options.root,
|
|
181
|
-
version,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
await program.parseAsync();
|
|
187
|
-
}
|
|
6
|
+
if (import.meta.main) await main();
|