@dbx-tools/projen 0.9.43 → 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 +28 -34
  2. package/index.ts +6 -7
  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 +1 -9
  18. package/src/codegen.ts +20 -17
  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 -157
  25. package/src/project-predicate.ts +7 -3
  26. package/src/project-py.ts +120 -43
  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 +30 -9
  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 +77 -21
  45. package/tasks/lib/python-registry.ts +4 -0
  46. package/tasks/lib/python-release.ts +2 -0
  47. package/tasks/lib/release.ts +1 -1
  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/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];",
@@ -316,7 +323,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
316
323
  target: "browser",
317
324
  });
318
325
  if (!result.success) {
319
- for (const message of result.logs) console.error(message);
326
+ for (const message of result.logs) logger.error(message);
320
327
  throw new Error(`Could not bundle ${config.entrypoint}`);
321
328
  }
322
329
  if (result.outputs.length !== 1) {
@@ -438,7 +445,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
438
445
  .map((name) => ` ${JSON.stringify(name)},`)
439
446
  .join("\n");
440
447
  const dataclasses = records.map(pythonDataclass).join("\n\n\n");
441
- const responseTypes = responses.map(pythonResponse).join("\n\n\n");
448
+ const responseTypes = orderPythonResponses(responses).map(pythonResponse).join("\n\n\n");
442
449
  const protocolTypes = protocols.map(pythonProtocol).join("\n\n\n");
443
450
  const wrappers = functions
444
451
  .map(({ async, javascriptName, pythonName, parameters, returnType }) =>
@@ -526,6 +533,39 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
526
533
  readonly type: string;
527
534
  }
528
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
+
529
569
  interface PythonProtocol {
530
570
  readonly methods: readonly PythonProtocolMethod[];
531
571
  readonly name: string;
@@ -658,10 +698,8 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
658
698
  const placeholder: PythonRecord = { name, fields: [] };
659
699
  records.set(name, placeholder);
660
700
  const fields = checker.getPropertiesOfType(type).map((property) => {
661
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
662
- if (!declaration) throw new Error(`${path}.${property.name} has no TypeScript declaration`);
663
701
  const propertyType = withoutUndefined(
664
- checker.getTypeOfSymbolAtLocation(property, declaration),
702
+ resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
665
703
  );
666
704
  return {
667
705
  javascriptName: property.name,
@@ -782,13 +820,15 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
782
820
  );
783
821
  }
784
822
  const properties = checker.getPropertiesOfType(type).filter(publicProperty);
785
- const methods = properties.filter((property) => {
786
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
787
- return Boolean(
788
- declaration &&
789
- checker.getTypeOfSymbolAtLocation(property, declaration).getCallSignatures().length,
790
- );
791
- });
823
+ const methods = properties.filter(
824
+ (property) =>
825
+ resolvedPropertyType(
826
+ checker,
827
+ type,
828
+ property,
829
+ `${path}.${property.name}`,
830
+ ).getCallSignatures().length > 0,
831
+ );
792
832
  if (methods.length > 0) {
793
833
  const name = pythonObjectName(type, path, "Result");
794
834
  if (protocols.has(name)) return name;
@@ -847,14 +887,14 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
847
887
  const protocolProperties = properties
848
888
  .filter(({ name }) => !methodNames.has(name))
849
889
  .map((property) => {
850
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
851
- if (!declaration) throw new Error(`${path}.${property.name} has no declaration`);
852
890
  return {
853
891
  name: pythonFunctionName(property.name),
854
892
  required: !(property.flags & ts.SymbolFlags.Optional),
855
893
  type: pythonReturnType(
856
894
  checker,
857
- withoutUndefined(checker.getTypeOfSymbolAtLocation(property, declaration)),
895
+ withoutUndefined(
896
+ resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
897
+ ),
858
898
  responses,
859
899
  protocols,
860
900
  `${path}.${property.name}`,
@@ -868,14 +908,14 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
868
908
  if (responses.has(name)) return name;
869
909
  responses.set(name, { name, fields: [] });
870
910
  const fields = properties.map((property) => {
871
- const declaration = property.valueDeclaration ?? property.declarations?.[0];
872
- if (!declaration) throw new Error(`${path}.${property.name} has no TypeScript declaration`);
873
911
  return {
874
912
  name: property.name,
875
913
  required: !(property.flags & ts.SymbolFlags.Optional),
876
914
  type: pythonReturnType(
877
915
  checker,
878
- withoutUndefined(checker.getTypeOfSymbolAtLocation(property, declaration)),
916
+ withoutUndefined(
917
+ resolvedPropertyType(checker, type, property, `${path}.${property.name}`),
918
+ ),
879
919
  responses,
880
920
  protocols,
881
921
  `${path}.${property.name}`,
@@ -908,12 +948,28 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
908
948
  }
909
949
 
910
950
  function publicProperty(property: ts.Symbol): boolean {
951
+ if (!property.declarations?.length) return true;
911
952
  return (property.declarations ?? []).some((declaration) => {
912
953
  const flags = ts.getCombinedModifierFlags(declaration as ts.Declaration);
913
954
  return !(flags & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected));
914
955
  });
915
956
  }
916
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
+
917
973
  function pythonObjectName(type: ts.Type, path: string, suffix: string): string {
918
974
  const name = type.aliasSymbol?.getName() ?? type.getSymbol()?.getName();
919
975
  if (name && name !== "__type") return name;
@@ -1096,7 +1152,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
1096
1152
  function writeGenerated(output: string, contents: string, kind: string): void {
1097
1153
  const destination = relative(root, output);
1098
1154
  if (existsSync(output) && readFileSync(output, "utf8") === contents) {
1099
- if (values.check) console.log(`verified ${destination}`);
1155
+ if (values.check) logger.info(`verified ${destination}`);
1100
1156
  return;
1101
1157
  }
1102
1158
  if (values.check) {
@@ -1106,7 +1162,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
1106
1162
  makeWritable(output);
1107
1163
  writeFileSync(output, contents);
1108
1164
  makeReadonly(output);
1109
- console.log(`generated ${destination}`);
1165
+ logger.info(`generated ${destination}`);
1110
1166
  }
1111
1167
  }
1112
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: {
@@ -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";
package/tasks/sync.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env -S bun
2
+ /** Synthesize once or supervise the focused Projen and barrel watch loops. */
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { log } from "@dbx-tools/shared-core";
4
5
  import concurrently from "concurrently";
@@ -46,8 +47,8 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
46
47
  // Watch: one initial full synth to bring the tree up to date, then focused
47
48
  // watchers under `concurrently`. The projenrc watcher is the intelligent stand-in
48
49
  // for stock `projen --watch` - it re-synths (+install) ONLY when `.projenrc.ts` or
49
- // a configured `syncResynthPaths` entry changes, while barrels/openapi keep generated
50
- // OUTPUT fresh on source edits with no full synth.
50
+ // a configured `syncResynthPaths` entry changes, while the barrel watcher keeps
51
+ // generated package exports fresh on source edits with no full synth.
51
52
  //
52
53
  // VS Code auto-runs this on folder open, so from here down nothing is allowed to be
53
54
  // fatal: errors are logged and retried, and only a stop signal ends the task.
@@ -67,11 +68,10 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
67
68
  const watchers = [
68
69
  watcher("projenrc.ts", "projenrc", "magenta"),
69
70
  watcher("barrels.ts", "barrels", "cyan", "--watch"),
70
- watcher("openapi.ts", "openapi", "green", "--watch"),
71
71
  ];
72
72
  const { result } = concurrently(watchers, {
73
73
  prefix: "name",
74
- // No `killOthersOn`: one watcher falling over is no reason to tear the other two
74
+ // No `killOthersOn`: one watcher falling over is no reason to tear the other
75
75
  // down. `-1` is concurrently's spelling for "restart forever", so a crashed
76
76
  // watcher comes back instead of silently leaving its outputs stale.
77
77
  restartTries: -1,