@dbx-tools/projen 0.9.40 → 0.9.46

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 (52) hide show
  1. package/README.md +58 -67
  2. package/index.ts +6 -8
  3. package/package.json +9 -10
  4. package/shims/python-node/bootstrap.ts +129 -0
  5. package/shims/python-node/child-process.ts +96 -0
  6. package/shims/python-node/crypto.ts +60 -0
  7. package/shims/python-node/fs-promises.ts +179 -0
  8. package/shims/python-node/fs.ts +173 -0
  9. package/shims/python-node/headers.ts +75 -0
  10. package/shims/python-node/host.ts +341 -0
  11. package/shims/python-node/os.ts +20 -0
  12. package/shims/python-node/process.ts +13 -0
  13. package/shims/python-node/readline.ts +37 -0
  14. package/shims/python-node/tsconfig.json +24 -0
  15. package/src/_lazy-require.ts +7 -25
  16. package/src/bun-app.ts +2 -2
  17. package/src/bun-workflow.ts +2 -170
  18. package/src/codegen.ts +23 -20
  19. package/src/dbx-tools-config.ts +52 -9
  20. package/src/generated.ts +4 -17
  21. package/src/mixin.ts +1 -5
  22. package/src/packages.ts +18 -15
  23. package/src/pnpm-workspace.ts +1 -4
  24. package/src/project-js.ts +65 -196
  25. package/src/project-predicate.ts +7 -3
  26. package/src/project-py.ts +127 -37
  27. package/src/publish.ts +13 -10
  28. package/src/python-node-bindings.ts +2 -0
  29. package/src/python-node-bundle.ts +6 -0
  30. package/src/release-options.ts +17 -17
  31. package/src/release.ts +31 -14
  32. package/src/scaffold.ts +4 -7
  33. package/src/tags.ts +6 -16
  34. package/src/vscode.ts +1 -1
  35. package/src/watch.ts +5 -5
  36. package/src/workspace-lock.ts +1 -0
  37. package/src/workspace-version.ts +4 -4
  38. package/tasks/barrels.ts +3 -5
  39. package/tasks/clean.ts +1 -0
  40. package/tasks/lib/compile-workspace.ts +41 -19
  41. package/tasks/lib/local-publish.ts +0 -28
  42. package/tasks/lib/publish-npm.ts +8 -0
  43. package/tasks/lib/publish-python.ts +4 -0
  44. package/tasks/lib/python-node-bindings.ts +107 -26
  45. package/tasks/lib/python-registry.ts +4 -0
  46. package/tasks/lib/python-release.ts +2 -0
  47. package/tasks/lib/release.ts +2 -2
  48. package/tasks/projenrc.ts +1 -0
  49. package/tasks/sync.ts +4 -4
  50. package/src/openapi.ts +0 -293
  51. package/tasks/local-publish.ts +0 -6
  52. package/tasks/openapi.ts +0 -35
package/src/vscode.ts CHANGED
@@ -33,7 +33,7 @@ export function configureVsCode(scope: javascript.NodeProject): void {
33
33
  {
34
34
  label: "sync",
35
35
  detail:
36
- "projen sync --watch - projenrc (.projenrc.ts + syncResynthPaths re-synth) + barrels + openapi watchers",
36
+ "projen sync --watch - projenrc (.projenrc.ts + syncResynthPaths re-synth) + barrel watcher",
37
37
  type: "shell",
38
38
  command: "bun run sync -- --watch",
39
39
  isBackground: true,
package/src/watch.ts CHANGED
@@ -7,13 +7,13 @@
7
7
  * decls - reacting to our own output would loop), exits on a watcher error chokidar
8
8
  * cannot recover from ({@link FATAL_WATCH_ERROR_CODES}), and shuts down on SIGINT. Callers
9
9
  * pass the paths to watch and an `onBatch` handler; the concern-specific glue - which
10
- * barrels to rebuild, when to regenerate openapi, when to re-synth - lives in the task
11
- * that owns it (`tasks/barrels.ts`, `tasks/openapi.ts`, `tasks/projenrc.ts`), each
12
- * forwarding here rather than duplicating the watch machinery.
10
+ * barrels to rebuild or when to re-synth - lives in the task that owns it
11
+ * (`tasks/barrels.ts` or `tasks/projenrc.ts`), each forwarding here rather than
12
+ * duplicating the watch machinery.
13
13
  *
14
14
  * `watchRoots()` is the one shared input - the package roots where every
15
- * watchable source file lives - so the barrels and openapi watchers don't each
16
- * recompute it. {@link watchFiles} owns the chokidar wiring; this is thin glue.
15
+ * watchable source file lives. {@link watchFiles} owns the chokidar wiring; this
16
+ * is thin glue.
17
17
  */
18
18
  import { isAbsolute, resolve } from "node:path";
19
19
  import { watch as pathWatch } from "@dbx-tools/path";
@@ -6,6 +6,7 @@ import { log } from "@dbx-tools/shared-core";
6
6
  const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
7
7
  const logger = log.logger("projen:workspace-lock");
8
8
 
9
+ /** Optional preflight used before and after acquiring the workspace mutation lock. */
9
10
  export interface WorkspaceMutationLockOptions {
10
11
  /**
11
12
  * When this returns false, skip both the lock and the callback. After the
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * The repo-root `VERSION` file holds one plain `x.y.z` string that every
5
5
  * generated manifest copies at synth: the root and `projen/` package.json, every
6
- * JS member, every Python `pyproject.toml`, the generated openapi packages, and
7
- * the example apps. Synth only READS this file (defaulting to {@link
8
- * DEFAULT_VERSION} when it is absent on a fresh tree); it never rewrites it, so an
9
- * ordinary `bunx projen` cannot move a package version up or down.
6
+ * JavaScript member, every Python `pyproject.toml`, and the example apps. Synth
7
+ * only READS this file (defaulting to {@link DEFAULT_VERSION} when it is absent
8
+ * on a fresh tree); it never rewrites it, so an ordinary `bunx projen` cannot
9
+ * move a package version up or down.
10
10
  *
11
11
  * Only the pure `bump` task changes the number. It increments the checked-in
12
12
  * file while release preparation owns the surrounding Git transaction.
package/tasks/barrels.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env -S bun
2
+ /** Generate package barrels once or keep affected barrels current in watch mode. */
2
3
  import { sep } from "node:path";
3
4
  import { parseArgs } from "node:util";
4
5
  import { log, object, stringUtils } from "@dbx-tools/shared-core";
@@ -11,12 +12,9 @@ const logger = log.logger("projen:barrels");
11
12
  /** The recorded package dir that owns `abs`, if any (for a targeted barrel rebuild). */
12
13
  function ownerPackageDir(
13
14
  abs: string,
14
- packages: readonly { readonly dir: string; readonly tags: readonly string[] }[],
15
+ packages: readonly { readonly dir: string }[],
15
16
  ): string | undefined {
16
- // The OpenAPI task rebuilds generated-client barrels after writing its files.
17
- return packages.find(
18
- ({ dir, tags }) => !tags.includes("openapi") && (abs === dir || abs.startsWith(dir + sep)),
19
- )?.dir;
17
+ return packages.find(({ dir }) => abs === dir || abs.startsWith(dir + sep))?.dir;
20
18
  }
21
19
 
22
20
  function changedBarrelTargets(changed: readonly string[]): object.Sequence<string> {
package/tasks/clean.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env -S bun
2
+ /** Interactively remove generated files and installed dependency directories. */
2
3
  import { relative } from "node:path";
3
4
  import { log, stringUtils } from "@dbx-tools/shared-core";
4
5
  import { listGeneratedFiles, listNodeModulesDirs, removePaths } from "../src/clean.ts";
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env -S bun
2
- /** Compile source-first workspace packages in a few TypeScript processes. */
2
+ /** Batch package type-checks and run package-owned compile lifecycles. */
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { availableParallelism } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import * as exec from "@dbx-tools/core/exec";
7
+ import { log } from "@dbx-tools/shared-core";
8
+
9
+ const logger = log.logger("projen:compile");
7
10
 
8
11
  interface PackageManifest {
9
12
  readonly name?: string;
@@ -13,8 +16,14 @@ interface PackageManifest {
13
16
 
14
17
  interface TaskManifest {
15
18
  readonly tasks?: {
19
+ readonly "pre-compile"?: {
20
+ readonly steps?: Array<{ readonly exec?: string; readonly execArgs?: string[] }>;
21
+ };
16
22
  readonly compile?: {
17
- readonly steps?: Array<{ readonly execArgs?: string[] }>;
23
+ readonly steps?: Array<{ readonly exec?: string; readonly execArgs?: string[] }>;
24
+ };
25
+ readonly "post-compile"?: {
26
+ readonly steps?: Array<{ readonly exec?: string; readonly execArgs?: string[] }>;
18
27
  };
19
28
  };
20
29
  }
@@ -40,6 +49,10 @@ function plainTypeScriptBuild(steps: unknown): boolean {
40
49
  );
41
50
  }
42
51
 
52
+ function hasSteps(steps: unknown): boolean {
53
+ return Array.isArray(steps) && steps.length > 0;
54
+ }
55
+
43
56
  /** Read the root's concrete workspace paths on every run, including newly added members. */
44
57
  export function compilePlan(root: string): CompilePlan {
45
58
  const workspace = readJson<PackageManifest>(join(root, "package.json"));
@@ -51,14 +64,13 @@ export function compilePlan(root: string): CompilePlan {
51
64
  const script = manifest.scripts?.compile;
52
65
  if (!script) continue;
53
66
  const taskPath = join(directory, ".projen", "tasks.json");
54
- const steps = existsSync(taskPath)
55
- ? readJson<TaskManifest>(taskPath).tasks?.compile?.steps
56
- : undefined;
67
+ const tasks = existsSync(taskPath) ? readJson<TaskManifest>(taskPath).tasks : undefined;
68
+ const plainCompile = plainTypeScriptBuild(tasks?.compile?.steps);
57
69
  if (
58
70
  script === "projen compile" &&
59
- !manifest.scripts?.precompile &&
60
- !manifest.scripts?.postcompile &&
61
- plainTypeScriptBuild(steps)
71
+ plainCompile &&
72
+ !hasSteps(tasks?.["pre-compile"]?.steps) &&
73
+ !hasSteps(tasks?.["post-compile"]?.steps)
62
74
  ) {
63
75
  typescriptConfigs.push(join(directory, "tsconfig.json"));
64
76
  } else {
@@ -78,9 +90,7 @@ export async function main(): Promise<void> {
78
90
  const batchCount = Math.min(4, availableParallelism(), plan.typescriptConfigs.length);
79
91
  const batches = Array.from({ length: batchCount }, () => [] as string[]);
80
92
  plan.typescriptConfigs.forEach((config, index) => batches[index % batchCount]!.push(config));
81
- console.log(
82
- `Type-checking ${plan.typescriptConfigs.length} workspaces in ${batchCount} batches`,
83
- );
93
+ logger.info(`type-checking ${plan.typescriptConfigs.length} workspaces in ${batchCount} batches`);
84
94
  for (const [index, batch] of batches.entries()) {
85
95
  jobs.push({
86
96
  name: `TypeScript batch ${index + 1}`,
@@ -96,24 +106,36 @@ export async function main(): Promise<void> {
96
106
  for (const pkg of plan.customPackages) {
97
107
  jobs.push({
98
108
  name: pkg.name,
99
- result: exec.spawn("bun", ["run", "compile"], {
100
- cwd: pkg.directory,
101
- stdin: "ignore",
102
- stdout: (line) => console.log(`${pkg.name}: ${line.replace(emoji, "")}`),
103
- stderr: (line) => console.error(`${pkg.name}: ${line.replace(emoji, "")}`),
104
- }),
109
+ result: compileCustomPackage(pkg),
105
110
  });
106
111
  }
107
112
  const results = await Promise.allSettled(jobs.map((job) => job.result));
108
113
  let failed = false;
109
114
  for (const [index, result] of results.entries()) {
110
115
  if (result.status === "rejected") {
111
- console.error(`${jobs[index]!.name} compile failed:`, result.reason);
116
+ logger.error(`${jobs[index]!.name} compile failed:`, result.reason);
112
117
  failed = true;
113
118
  } else if (result.value.exitCode !== 0) {
114
- console.error(`${jobs[index]!.name} compile exited with ${result.value.exitCode}`);
119
+ logger.error(`${jobs[index]!.name} compile exited with ${result.value.exitCode}`);
115
120
  failed = true;
116
121
  }
117
122
  }
118
123
  if (failed) process.exitCode = 1;
119
124
  }
125
+
126
+ async function compileCustomPackage(pkg: {
127
+ readonly name: string;
128
+ readonly directory: string;
129
+ }): Promise<exec.ExecResult> {
130
+ let result: exec.ExecResult | undefined;
131
+ for (const task of ["pre-compile", "compile", "post-compile"]) {
132
+ result = await exec.spawn("bun", ["run", task], {
133
+ cwd: pkg.directory,
134
+ stdin: "ignore",
135
+ stdout: (line) => logger.info(`${pkg.name}: ${line.replace(emoji, "")}`),
136
+ stderr: (line) => logger.error(`${pkg.name}: ${line.replace(emoji, "")}`),
137
+ });
138
+ if (result.exitCode !== 0) return result;
139
+ }
140
+ return result!;
141
+ }
@@ -3,7 +3,6 @@ import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import * as projectUtils from "@dbx-tools/core/project-utils";
5
5
  import { log, net } from "@dbx-tools/shared-core";
6
- import { Command } from "commander";
7
6
  import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
8
7
  import { runTaskCommandAsync } from "../../src/_task-command.ts";
9
8
 
@@ -93,30 +92,3 @@ export async function publishLocalRelease(
93
92
  logger.success(`published local workspace ${options.version}`);
94
93
  return { npm: Boolean(localRegistry), python: Boolean(localPypi) };
95
94
  }
96
-
97
- export async function main(): Promise<void> {
98
- await new Command()
99
- .requiredOption("--version <version>", "release version")
100
- .option("--root <path>", "repository root")
101
- .option("--python-root <path>", "Python package root", "packages/py")
102
- .option("--local-registry <url>", "local npm registry", "auto")
103
- .option("--local-pypi <url>", "local devpi index", "auto")
104
- .action(
105
- async (options: {
106
- version: string;
107
- root?: string;
108
- pythonRoot: string;
109
- localRegistry: string;
110
- localPypi: string;
111
- }) => {
112
- await publishLocalRelease({
113
- localPypi: options.localPypi,
114
- localRegistry: options.localRegistry,
115
- pythonRoot: options.pythonRoot,
116
- root: options.root ?? projectUtils.root() ?? process.cwd(),
117
- version: options.version,
118
- });
119
- },
120
- )
121
- .parseAsync();
122
- }
@@ -22,6 +22,7 @@ import { runTaskCommand } from "../../src/_task-command.ts";
22
22
  const DEFAULT_REGISTRY = "https://registry.npmjs.org";
23
23
  const logger = log.logger("projen:publish-npm");
24
24
 
25
+ /** Registry and archive identity used to verify idempotent npm publication. */
25
26
  export interface NpmReleaseIdentity {
26
27
  readonly access?: "public" | "restricted";
27
28
  readonly contentDigest?: string;
@@ -41,6 +42,7 @@ interface NpmArchiveManifest {
41
42
  readonly version?: string;
42
43
  }
43
44
 
45
+ /** Transform applied to a packed npm manifest before archive verification. */
44
46
  export type NpmArchiveManifestTransform = (
45
47
  manifest: NpmArchiveManifest,
46
48
  ) => Readonly<Record<string, unknown>>;
@@ -143,6 +145,7 @@ function registryUrl(registry: string, name: string, version: string): string {
143
145
  return `${registry.replace(/\/$/, "")}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`;
144
146
  }
145
147
 
148
+ /** Verify that registry metadata represents the exact locally packed release. */
146
149
  export function npmReleaseMatches(
147
150
  local: NpmReleaseIdentity,
148
151
  published: NpmReleaseIdentity | undefined,
@@ -173,6 +176,7 @@ export function npmReleaseMatches(
173
176
  return true;
174
177
  }
175
178
 
179
+ /** Read one published npm release identity, or `undefined` when it is absent. */
176
180
  export async function publishedNpmRelease(
177
181
  name: string,
178
182
  version: string,
@@ -232,6 +236,7 @@ function readNpmArchiveManifest(path: string): NpmArchiveManifest {
232
236
  return JSON.parse(result.stdout) as NpmArchiveManifest;
233
237
  }
234
238
 
239
+ /** Read and hash the release identity embedded in an npm archive. */
235
240
  export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
236
241
  const manifest = readNpmArchiveManifest(path);
237
242
  if (!manifest.name || !manifest.version) {
@@ -326,6 +331,7 @@ export function npmArchiveContentDigest(path: string): string {
326
331
  }
327
332
  }
328
333
 
334
+ /** Pack one workspace package and optionally transform only its archived manifest. */
329
335
  export function packNpmPackage(
330
336
  directory: string,
331
337
  destination: string,
@@ -404,6 +410,7 @@ export function packNpmPackage(
404
410
  }
405
411
  }
406
412
 
413
+ /** Publish dependency-ordered npm archives, skipping only exact registry matches. */
407
414
  export async function publishNpmArchives(options: {
408
415
  readonly directory: string;
409
416
  readonly dryRun?: boolean;
@@ -443,6 +450,7 @@ export async function publishNpmArchives(options: {
443
450
  }
444
451
  }
445
452
 
453
+ /** Parse npm archive publication options and execute the publisher. */
446
454
  export async function main(): Promise<void> {
447
455
  const program = new Command();
448
456
  program
@@ -1,4 +1,8 @@
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
+ */
2
6
  import {
3
7
  chmodSync,
4
8
  cpSync,
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env -S bun
2
+ /**
3
+ * Generate a shared PythonMonkey runtime and typed Python wrappers from each
4
+ * package's serialized node-binding configuration.
5
+ */
2
6
  import {
3
7
  existsSync,
4
8
  mkdirSync,
@@ -10,7 +14,7 @@ import {
10
14
  import { dirname, join, relative, resolve } from "node:path";
11
15
  import { fileURLToPath, pathToFileURL } from "node:url";
12
16
  import { parseArgs } from "node:util";
13
- import { stringUtils } from "@dbx-tools/shared-core";
17
+ import { log, stringUtils } from "@dbx-tools/shared-core";
14
18
  import type { BunPlugin } from "bun";
15
19
  import stdLibBrowser from "node-stdlib-browser";
16
20
  import ts from "typescript";
@@ -23,6 +27,8 @@ import {
23
27
  type ResolvedPythonNodeFunctionOverride,
24
28
  } from "../../src/python-node-bindings.ts";
25
29
 
30
+ const logger = log.logger("projen:python-node-bindings");
31
+
26
32
  type FunctionOverride = ResolvedPythonNodeFunctionOverride;
27
33
 
28
34
  function bindingIndex(value: string, count: number, project: string): number {
@@ -180,6 +186,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
180
186
  ["fs/promises", resolve(shimRoot, "fs-promises.ts")],
181
187
  ["os", resolve(shimRoot, "os.ts")],
182
188
  ["process", resolve(shimRoot, "process.ts")],
189
+ ["readline", resolve(shimRoot, "readline.ts")],
183
190
  ]);
184
191
  const bridgeSource = [
185
192
  "export const __pythonGet = (target, name) => target[name];",
@@ -270,6 +277,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
270
277
  };
271
278
  });
272
279
  build.onResolve({ filter: /.*/ }, ({ path }) => {
280
+ if (path === "zod") {
281
+ return {
282
+ path: resolve(dirname(Bun.resolveSync("zod/package.json", root)), "index.cjs"),
283
+ };
284
+ }
273
285
  if (overridesByModule.has(path)) {
274
286
  return { path, namespace: functionOverrideNamespace };
275
287
  }
@@ -311,7 +323,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
311
323
  target: "browser",
312
324
  });
313
325
  if (!result.success) {
314
- for (const message of result.logs) console.error(message);
326
+ for (const message of result.logs) logger.error(message);
315
327
  throw new Error(`Could not bundle ${config.entrypoint}`);
316
328
  }
317
329
  if (result.outputs.length !== 1) {
@@ -433,7 +445,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
433
445
  .map((name) => ` ${JSON.stringify(name)},`)
434
446
  .join("\n");
435
447
  const dataclasses = records.map(pythonDataclass).join("\n\n\n");
436
- const responseTypes = responses.map(pythonResponse).join("\n\n\n");
448
+ const responseTypes = orderPythonResponses(responses).map(pythonResponse).join("\n\n\n");
437
449
  const protocolTypes = protocols.map(pythonProtocol).join("\n\n\n");
438
450
  const wrappers = functions
439
451
  .map(({ async, javascriptName, pythonName, parameters, returnType }) =>
@@ -521,6 +533,39 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
521
533
  readonly type: string;
522
534
  }
523
535
 
536
+ /**
537
+ * Emit nested response types before the responses that reference them.
538
+ *
539
+ * Functional TypedDict declarations evaluate their field types immediately,
540
+ * unlike class annotations protected by `from __future__ import annotations`.
541
+ */
542
+ function orderPythonResponses(responses: readonly PythonResponse[]): PythonResponse[] {
543
+ const ordered: PythonResponse[] = [];
544
+ const visited = new Set<string>();
545
+ const visiting = new Set<string>();
546
+
547
+ const visit = (response: PythonResponse): void => {
548
+ if (visited.has(response.name) || visiting.has(response.name)) return;
549
+ visiting.add(response.name);
550
+ for (const dependency of responses) {
551
+ if (
552
+ dependency.name !== response.name &&
553
+ response.fields.some(({ type }) =>
554
+ new RegExp(`\\b${dependency.name}\\b`).test(type),
555
+ )
556
+ ) {
557
+ visit(dependency);
558
+ }
559
+ }
560
+ visiting.delete(response.name);
561
+ visited.add(response.name);
562
+ ordered.push(response);
563
+ };
564
+
565
+ for (const response of responses) visit(response);
566
+ return ordered;
567
+ }
568
+
524
569
  interface PythonProtocol {
525
570
  readonly methods: readonly PythonProtocolMethod[];
526
571
  readonly name: string;
@@ -586,7 +631,13 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
586
631
  }
587
632
  const parameterType = withoutUndefined(checker.getTypeAtLocation(parameter));
588
633
  const path = `${exported.sourceName}.${parameter.name.text}`;
589
- const record = pythonRecordType(checker, parameterType, records, path);
634
+ const record = pythonRecordType(
635
+ checker,
636
+ parameterType,
637
+ records,
638
+ path,
639
+ declaredTypeName(parameter.type),
640
+ );
590
641
  const required = !parameter.questionToken && !parameter.initializer;
591
642
  return {
592
643
  javascriptName: parameter.name.text,
@@ -606,6 +657,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
606
657
  responses,
607
658
  protocols,
608
659
  `${exported.sourceName}.return`,
660
+ declaredTypeName(declaration.type),
609
661
  );
610
662
  typesByFunction.set(`${exported.sourceFile}#${exported.sourceName}`, {
611
663
  parameters,
@@ -636,18 +688,18 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
636
688
  type: ts.Type,
637
689
  records: Map<string, PythonRecord>,
638
690
  path: string,
691
+ preferredName?: string,
639
692
  ): PythonRecord {
640
- const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
693
+ const symbolName = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
694
+ const name = symbolName && symbolName !== "__type" ? symbolName : preferredName;
641
695
  if (!name || name === "__type") throw new Error(`${path} must reference a named object type`);
642
696
  const existing = records.get(name);
643
697
  if (existing) return existing;
644
698
  const placeholder: PythonRecord = { name, fields: [] };
645
699
  records.set(name, placeholder);
646
700
  const fields = checker.getPropertiesOfType(type).map((property) => {
647
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
648
- if (!declaration) throw new Error(`${path}.${property.name} has no TypeScript declaration`);
649
701
  const propertyType = withoutUndefined(
650
- checker.getTypeOfSymbolAtLocation(property, declaration),
702
+ resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
651
703
  );
652
704
  return {
653
705
  javascriptName: property.name,
@@ -665,13 +717,23 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
665
717
  type: ts.Type,
666
718
  records: Map<string, PythonRecord>,
667
719
  path: string,
720
+ preferredName?: string,
668
721
  ): PythonRecord | undefined {
669
722
  if (!(type.flags & ts.TypeFlags.Object)) return undefined;
670
723
  if (checker.isArrayType(type) || type.getCallSignatures().length > 0) return undefined;
671
724
  if (checker.getIndexTypeOfType(type, ts.IndexKind.String)) return undefined;
672
- const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
725
+ const symbolName = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
726
+ const name = symbolName && symbolName !== "__type" ? symbolName : preferredName;
673
727
  if (!name || name === "__type") return undefined;
674
- return pythonRecord(checker, type, records, path);
728
+ return pythonRecord(checker, type, records, path, preferredName);
729
+ }
730
+
731
+ function declaredTypeName(node: ts.TypeNode | undefined): string | undefined {
732
+ if (!node) return undefined;
733
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
734
+ return node.typeName.text;
735
+ }
736
+ return undefined;
675
737
  }
676
738
 
677
739
  function pythonType(
@@ -715,6 +777,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
715
777
  responses: Map<string, PythonResponse>,
716
778
  protocols: Map<string, PythonProtocol>,
717
779
  path: string,
780
+ preferredName?: string,
718
781
  ): string {
719
782
  if (type.isUnion()) {
720
783
  return pythonUnion(
@@ -757,13 +820,15 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
757
820
  );
758
821
  }
759
822
  const properties = checker.getPropertiesOfType(type).filter(publicProperty);
760
- const methods = properties.filter((property) => {
761
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
762
- return Boolean(
763
- declaration &&
764
- checker.getTypeOfSymbolAtLocation(property, declaration).getCallSignatures().length,
765
- );
766
- });
823
+ const methods = properties.filter(
824
+ (property) =>
825
+ resolvedPropertyType(
826
+ checker,
827
+ type,
828
+ property,
829
+ `${path}.${property.name}`,
830
+ ).getCallSignatures().length > 0,
831
+ );
767
832
  if (methods.length > 0) {
768
833
  const name = pythonObjectName(type, path, "Result");
769
834
  if (protocols.has(name)) return name;
@@ -822,14 +887,14 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
822
887
  const protocolProperties = properties
823
888
  .filter(({ name }) => !methodNames.has(name))
824
889
  .map((property) => {
825
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
826
- if (!declaration) throw new Error(`${path}.${property.name} has no declaration`);
827
890
  return {
828
891
  name: pythonFunctionName(property.name),
829
892
  required: !(property.flags & ts.SymbolFlags.Optional),
830
893
  type: pythonReturnType(
831
894
  checker,
832
- withoutUndefined(checker.getTypeOfSymbolAtLocation(property, declaration)),
895
+ withoutUndefined(
896
+ resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
897
+ ),
833
898
  responses,
834
899
  protocols,
835
900
  `${path}.${property.name}`,
@@ -839,18 +904,18 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
839
904
  protocols.set(name, { name, methods: protocolMethods, properties: protocolProperties });
840
905
  return name;
841
906
  }
842
- const name = `${pythonObjectName(type, path, "Result")}Response`;
907
+ const name = `${preferredName ?? pythonObjectName(type, path, "Result")}Response`;
843
908
  if (responses.has(name)) return name;
844
909
  responses.set(name, { name, fields: [] });
845
910
  const fields = properties.map((property) => {
846
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
847
- if (!declaration) throw new Error(`${path}.${property.name} has no TypeScript declaration`);
848
911
  return {
849
912
  name: property.name,
850
913
  required: !(property.flags & ts.SymbolFlags.Optional),
851
914
  type: pythonReturnType(
852
915
  checker,
853
- withoutUndefined(checker.getTypeOfSymbolAtLocation(property, declaration)),
916
+ withoutUndefined(
917
+ resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
918
+ ),
854
919
  responses,
855
920
  protocols,
856
921
  `${path}.${property.name}`,
@@ -883,12 +948,28 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
883
948
  }
884
949
 
885
950
  function publicProperty(property: ts.Symbol): boolean {
951
+ if (!property.declarations?.length) return true;
886
952
  return (property.declarations ?? []).some((declaration) => {
887
953
  const flags = ts.getCombinedModifierFlags(declaration as ts.Declaration);
888
954
  return !(flags & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected));
889
955
  });
890
956
  }
891
957
 
958
+ function resolvedPropertyType(
959
+ checker: ts.TypeChecker,
960
+ owner: ts.Type,
961
+ property: ts.Symbol,
962
+ path: string,
963
+ ): ts.Type {
964
+ const location =
965
+ property.valueDeclaration ??
966
+ property.declarations?.[0] ??
967
+ owner.aliasSymbol?.declarations?.[0] ??
968
+ owner.getSymbol()?.declarations?.[0];
969
+ if (!location) throw new Error(`${path} has no TypeScript type location`);
970
+ return checker.getTypeOfSymbolAtLocation(property, location);
971
+ }
972
+
892
973
  function pythonObjectName(type: ts.Type, path: string, suffix: string): string {
893
974
  const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
894
975
  if (name && name !== "__type") return name;
@@ -1071,7 +1152,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
1071
1152
  function writeGenerated(output: string, contents: string, kind: string): void {
1072
1153
  const destination = relative(root, output);
1073
1154
  if (existsSync(output) && readFileSync(output, "utf8") === contents) {
1074
- if (values.check) console.log(`verified ${destination}`);
1155
+ if (values.check) logger.info(`verified ${destination}`);
1075
1156
  return;
1076
1157
  }
1077
1158
  if (values.check) {
@@ -1081,7 +1162,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
1081
1162
  makeWritable(output);
1082
1163
  writeFileSync(output, contents);
1083
1164
  makeReadonly(output);
1084
- console.log(`generated ${destination}`);
1165
+ logger.info(`generated ${destination}`);
1085
1166
  }
1086
1167
  }
1087
1168
 
@@ -1,3 +1,4 @@
1
+ /** Resolve active uv indexes and writable loopback devpi publication targets. */
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { resolve } from "node:path";
@@ -5,8 +6,11 @@ import { net } from "@dbx-tools/shared-core";
5
6
  import { parse } from "smol-toml";
6
7
  import { captureTaskCommand } from "../../src/_task-command.ts";
7
8
 
9
+ /** Paired Python package index and upload endpoints for a local registry. */
8
10
  export interface LocalPythonRegistry {
11
+ /** PEP 503 package index URL used for dependency resolution. */
9
12
  readonly indexUrl: string;
13
+ /** Registry upload URL used for publication. */
10
14
  readonly publishUrl: string;
11
15
  }
12
16
 
@@ -22,6 +22,7 @@ const directSibling = (
22
22
  return directory === sibling.directory || directory.endsWith(`/${sibling.directory}`);
23
23
  };
24
24
 
25
+ /** Parse the publication identity and private flag from one Python manifest. */
25
26
  export function pythonProjectInfo(
26
27
  source: string,
27
28
  toml: { parse(source: string): unknown },
@@ -41,6 +42,7 @@ export function pythonProjectInfo(
41
42
  };
42
43
  }
43
44
 
45
+ /** Project sibling dependencies and the release version into an isolated manifest copy. */
44
46
  export function preparePythonProjectForPublication(
45
47
  source: string,
46
48
  options: {
@@ -10,7 +10,6 @@ import {
10
10
  runTaskCommand,
11
11
  taskCommandSucceeds,
12
12
  } from "../../src/_task-command.ts";
13
- import { readWorkspaceVersion } from "../../src/workspace-version.ts";
14
13
  import {
15
14
  RELEASE_INSTALL_MODES,
16
15
  RELEASE_PUBLISH_TARGETS,
@@ -20,6 +19,7 @@ import {
20
19
  type ReleaseInstallMode,
21
20
  type ReleaseSelectionOptions,
22
21
  } from "../../src/release-options.ts";
22
+ import { readWorkspaceVersion } from "../../src/workspace-version.ts";
23
23
 
24
24
  const logger = log.logger("projen:release");
25
25
 
@@ -115,7 +115,7 @@ export async function runRelease(
115
115
  const selection = releaseStepSelection(options);
116
116
  const install = options.install ?? "auto";
117
117
 
118
- if (install === "always") runTaskCommand(root, "bun", ["install", "--frozen-lockfile"]);
118
+ if (install === "always") runTaskCommand(root, "bun", ["install"]);
119
119
 
120
120
  prepareReleaseBranch({ branch, remote, root });
121
121
 
package/tasks/projenrc.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env -S bun
2
+ /** Resynthesize when the root definition or configured generator inputs change. */
2
3
  import { resolve } from "node:path";
3
4
  import { log } from "@dbx-tools/shared-core";
4
5
  import { repoRoot, syncResynthPaths } from "../src/packages.ts";