@dbx-tools/projen 0.9.154 → 0.9.158

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.
Files changed (49) hide show
  1. package/README.md +36 -47
  2. package/index.ts +42 -20
  3. package/package.json +8 -8
  4. package/src/_task-command.ts +7 -7
  5. package/src/appkit-workspace.ts +278 -104
  6. package/src/barrels.ts +147 -259
  7. package/src/bun-app.ts +54 -10
  8. package/src/clean.ts +6 -6
  9. package/src/engine-root.ts +4 -4
  10. package/src/mixin.ts +0 -2
  11. package/src/module-exports.ts +4 -5
  12. package/src/npm-packaging.ts +128 -0
  13. package/src/npm-release.ts +200 -0
  14. package/src/packages.ts +11 -11
  15. package/src/project-js.ts +209 -103
  16. package/src/project-predicate.ts +5 -12
  17. package/src/project-py.ts +6 -6
  18. package/src/project.ts +7 -14
  19. package/src/publish.ts +3 -3
  20. package/src/python-node-bindings.ts +16 -12
  21. package/src/python-packaging.ts +177 -0
  22. package/src/release-options.ts +2 -2
  23. package/src/release.ts +3 -3
  24. package/src/shared-core-usage.ts +17 -4
  25. package/src/tags.ts +10 -9
  26. package/src/watch.ts +7 -9
  27. package/src/workspace-lock.ts +2 -2
  28. package/src/workspace-manifest.ts +2 -2
  29. package/tasks/appkit-deploy.ts +35 -15
  30. package/tasks/barrels.ts +6 -9
  31. package/tasks/bump.ts +1 -1
  32. package/tasks/clean.ts +3 -3
  33. package/tasks/cli.ts +3 -3
  34. package/tasks/compile-workspace.ts +8 -8
  35. package/tasks/dev-watch.ts +11 -11
  36. package/tasks/local-publish.ts +6 -6
  37. package/tasks/publish-npm.ts +11 -319
  38. package/tasks/publish-python.ts +2 -180
  39. package/tasks/publish.ts +6 -11
  40. package/tasks/python-node-bindings.ts +2 -2
  41. package/tasks/python-registry.ts +6 -6
  42. package/tasks/python-sync.ts +2 -3
  43. package/tasks/release-version.ts +1 -1
  44. package/tasks/release.ts +11 -11
  45. package/tasks/sync.ts +2 -2
  46. package/tasks/test-workspace.ts +9 -9
  47. package/tasks/verify-publications.ts +9 -5
  48. package/tasks/version-check.ts +5 -5
  49. /package/{tasks → src}/python-release.ts +0 -0
@@ -1,216 +1,22 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /** Idempotent npm archive publication for release recovery. */
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 * 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 { PUBLIC_NPM_REGISTRY_URL } from "../src/release-context.ts";
24
- import { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
25
-
26
- export { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
10
+ import {
11
+ npmReleaseMatches,
12
+ publishedNpmRelease,
13
+ readNpmArchiveIdentity,
14
+ readNpmArchiveManifest,
15
+ } from "../src/npm-release.ts";
27
16
 
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 ?? PUBLIC_NPM_REGISTRY_URL,
136
- signal?: AbortSignal,
137
- ): Promise<NpmReleaseIdentity | undefined> {
138
- const response = await fetch(registryUrl(registry, name, version), {
139
- headers: { accept: "application/json" },
140
- signal,
141
- });
142
- if (response.status === 404) return undefined;
143
- if (!response.ok) {
144
- throw new Error(`npm registry lookup failed for ${name}@${version}: ${response.status}`);
145
- }
146
- const metadata = (await response.json()) as {
147
- dist?: { integrity?: string; tarball?: string };
148
- name?: string;
149
- repository?: unknown;
150
- version?: string;
151
- };
152
- if (!metadata.name || !metadata.version) {
153
- throw new Error(`npm registry returned an incomplete identity for ${name}@${version}`);
154
- }
155
- let contentDigest: string | undefined;
156
- if (metadata.dist?.tarball) {
157
- const archive = await fetch(metadata.dist.tarball, { signal });
158
- if (!archive.ok) {
159
- throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
160
- }
161
- const temp = mkdtempSync(join(tmpdir(), "projen-published-npm-"));
162
- const path = join(temp, "package.tgz");
163
- try {
164
- writeFileSync(path, Buffer.from(await archive.arrayBuffer()));
165
- contentDigest = npmArchiveContentDigest(path);
166
- } finally {
167
- rmSync(temp, { recursive: true, force: true });
168
- }
169
- }
170
- return {
171
- ...(contentDigest ? { contentDigest } : {}),
172
- integrity: metadata.dist?.integrity,
173
- name: metadata.name,
174
- repository: metadata.repository,
175
- version: metadata.version,
176
- };
177
- }
178
-
179
- function readNpmArchiveManifest(path: string): NpmArchiveManifest {
180
- const result = exec.spawnSync("tar", ["-xOf", path, "package/package.json"], {
181
- cwd: process.cwd(),
182
- stdout: "capture",
183
- stderr: "capture",
184
- stdin: "ignore",
185
- check: false,
186
- });
187
- if (result.exitCode !== 0 || !result.stdout) {
188
- throw new Error(`Cannot read npm package manifest from ${path}: ${result.stderr}`);
189
- }
190
- return JSON.parse(result.stdout) as NpmArchiveManifest;
191
- }
192
-
193
- /** Read and hash the release identity embedded in an npm archive. */
194
- export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
195
- const manifest = readNpmArchiveManifest(path);
196
- if (!manifest.name || !manifest.version) {
197
- throw new Error(`npm archive has no package name or version: ${path}`);
198
- }
199
- const access = manifest.publishConfig?.access;
200
- if (access !== undefined && access !== "public" && access !== "restricted") {
201
- throw new Error(`npm archive has invalid publishConfig.access: ${String(access)}`);
202
- }
203
- return {
204
- ...(access ? { access } : {}),
205
- contentDigest: npmArchiveContentDigest(path),
206
- integrity: `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`,
207
- name: manifest.name,
208
- repository: manifest.repository,
209
- version: manifest.version,
210
- };
211
- }
212
-
213
- /** Group npm archives into dependency-ordered layers that can publish concurrently. */
214
20
  function npmArchiveLayers(archives: readonly string[]): string[][] {
215
21
  const byName = new Map(
216
22
  archives.map((archive) => {
@@ -243,9 +49,7 @@ function npmArchiveLayers(archives: readonly string[]): string[][] {
243
49
  );
244
50
  }
245
51
  layers.push(ready.map((name) => byName.get(name)!.archive));
246
- for (const name of ready) {
247
- remaining.delete(name);
248
- }
52
+ for (const name of ready) remaining.delete(name);
249
53
  for (const dependencies of remaining.values()) {
250
54
  for (const name of ready) dependencies.delete(name);
251
55
  }
@@ -253,118 +57,6 @@ function npmArchiveLayers(archives: readonly string[]): string[][] {
253
57
  return layers;
254
58
  }
255
59
 
256
- /** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
257
- export function npmArchiveContentDigest(path: string): string {
258
- const temp = mkdtempSync(join(tmpdir(), "projen-npm-content-"));
259
- try {
260
- exec.spawnSync("tar", ["-xzf", path, "-C", temp], {
261
- cwd: process.cwd(),
262
- stdout: "ignore",
263
- stderr: "capture",
264
- stdin: "ignore",
265
- check: true,
266
- });
267
- const hash = createHash("sha512");
268
- const visit = (directory: string): void => {
269
- for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
270
- a.name.localeCompare(b.name),
271
- )) {
272
- const entryPath = join(directory, entry.name);
273
- const name = relative(temp, entryPath).split("\\").join("/");
274
- const stat = lstatSync(entryPath);
275
- hash.update(`${entry.isDirectory() ? "D" : entry.isSymbolicLink() ? "L" : "F"}\0`);
276
- hash.update(`${name}\0${stat.mode & 0o111}\0`);
277
- if (entry.isDirectory()) visit(entryPath);
278
- else if (entry.isSymbolicLink()) hash.update(readlinkSync(entryPath));
279
- else hash.update(readFileSync(entryPath));
280
- }
281
- };
282
- visit(temp);
283
- return `sha512-${hash.digest("base64")}`;
284
- } finally {
285
- rmSync(temp, { recursive: true, force: true });
286
- }
287
- }
288
-
289
- /** Pack one workspace package and optionally transform only its archived manifest. */
290
- export function packNpmPackage(
291
- directory: string,
292
- destination: string,
293
- path = process.env.PATH ?? "",
294
- transformManifest?: NpmArchiveManifestTransform,
295
- ): string {
296
- const executable = process.versions.bun ? process.execPath : "bun";
297
- const manifest = join(directory, "package.json");
298
- const originalManifest = readFileSync(manifest, "utf8");
299
- const workspaceRoot = projectUtils.root(directory) ?? directory;
300
- const workspaceManifest = JSON.parse(
301
- readFileSync(join(workspaceRoot, "package.json"), "utf8"),
302
- ) as Record<string, unknown>;
303
- const packedManifest = `${JSON.stringify(
304
- materializeWorkspaceManifest(
305
- JSON.parse(originalManifest) as Record<string, unknown>,
306
- workspaceManifest,
307
- ),
308
- null,
309
- 2,
310
- )}\n`;
311
- const manifestMode = lstatSync(manifest).mode & 0o777;
312
- const restoreManifestMode = (manifestMode & 0o200) === 0;
313
- const existingArchives = new Set(readdirSync(destination));
314
- if (restoreManifestMode) chmodSync(manifest, manifestMode | 0o200);
315
- try {
316
- writeFileSync(manifest, packedManifest);
317
- exec.spawnSync(
318
- executable,
319
- ["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
320
- {
321
- cwd: directory,
322
- env: { ...process.env, PATH: path },
323
- stdout: "inherit",
324
- stderr: "inherit",
325
- stdin: "ignore",
326
- check: true,
327
- },
328
- );
329
- } finally {
330
- writeFileSync(manifest, originalManifest);
331
- if (restoreManifestMode) chmodSync(manifest, manifestMode);
332
- }
333
- const archives = readdirSync(destination).filter(
334
- (file) => file.endsWith(".tgz") && !existingArchives.has(file),
335
- );
336
- if (archives.length !== 1) {
337
- throw new Error(`Expected one packed npm archive, found ${archives.length}`);
338
- }
339
- const archive = join(destination, archives[0]);
340
- if (!transformManifest) return archive;
341
-
342
- const temp = mkdtempSync(join(tmpdir(), "projen-npm-project-"));
343
- try {
344
- exec.spawnSync("tar", ["-xzf", archive, "-C", temp], {
345
- cwd: process.cwd(),
346
- stdout: "ignore",
347
- stderr: "inherit",
348
- stdin: "ignore",
349
- check: true,
350
- });
351
- const manifestPath = join(temp, "package", "package.json");
352
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NpmArchiveManifest;
353
- writeFileSync(manifestPath, `${JSON.stringify(transformManifest(manifest), null, 2)}\n`);
354
- rmSync(archive, { force: true });
355
- exec.spawnSync("tar", ["-czf", archive, "-C", temp, ...archiveFileEntries(temp)], {
356
- cwd: process.cwd(),
357
- stdout: "ignore",
358
- stderr: "inherit",
359
- stdin: "ignore",
360
- check: true,
361
- });
362
- return archive;
363
- } finally {
364
- rmSync(temp, { recursive: true, force: true });
365
- }
366
- }
367
-
368
60
  /** Publish dependency-ordered npm archives, skipping only exact registry matches. */
369
61
  export async function publishNpmArchives(options: {
370
62
  readonly concurrency?: number;
@@ -383,7 +75,7 @@ export async function publishNpmArchives(options: {
383
75
  if (layers.length === 0) throw new Error(`No npm archives found in ${directory}`);
384
76
 
385
77
  for (const archives of layers) {
386
- await asyncUtils.mapConcurrent(
78
+ await mapConcurrent(
387
79
  archives,
388
80
  async (archive) => {
389
81
  const local = readNpmArchiveIdentity(archive);
@@ -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 { preparePythonProjectForPublication, pythonProjectInfo } from "./python-release.ts";
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 { asyncUtils, log, object } from "@dbx-tools/shared-core";
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 "./publish-npm.ts";
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 asyncUtils.mapConcurrent(
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, stringUtils } from "@dbx-tools/shared-core";
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 = stringUtils.toIdentifierWithOptions({ delimiter: "_" }, path);
952
+ const identifier = toIdentifierWithOptions({ delimiter: "_" }, path);
953
953
  return `${identifier
954
954
  .split("_")
955
955
  .filter(Boolean)
@@ -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 { net, stringUtils } from "@dbx-tools/shared-core";
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 (!net.isLoopbackHost(url)) return undefined;
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 stringUtils.parseList(process.env.UV_INDEX)) add(url);
151
- for (const url of stringUtils.parseList(process.env.UV_EXTRA_INDEX_URL)) add(url);
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 stringUtils.parseList(process.env.PIP_EXTRA_INDEX_URL)) add(url);
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 stringUtils.parseList(pipConfig("global.extra-index-url"))) add(url);
161
+ for (const url of parseList(pipConfig("global.extra-index-url"))) add(url);
162
162
 
163
163
  return out;
164
164
  }
@@ -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 * as exec from "@dbx-tools/core/exec";
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 } = exec.spawnSync(
255
+ const { stdout } = spawnSync(
257
256
  "uv",
258
257
  [
259
258
  "run",