@dbx-tools/projen 0.9.95 → 0.9.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/index.ts +1 -1
- package/package.json +4 -4
- package/src/project-js.ts +61 -14
- package/src/python-node-bindings.ts +279 -21
- package/src/release.ts +0 -10
- package/src/tsconfig.ts +1 -1
- package/tasks/python-node-bindings.ts +64 -100
- package/tasks/release-notes.ts +24 -1
- package/tasks/release.ts +30 -1
package/README.md
CHANGED
|
@@ -338,6 +338,22 @@ wrong fix, since the consumer authors no JSX and cannot know a transitive
|
|
|
338
338
|
dependency started to. The option is inert without JSX in the graph: it selects
|
|
339
339
|
how JSX syntax compiles and adds no lib, global, or type dependency.
|
|
340
340
|
|
|
341
|
+
Keep the generated tsconfigs package-local. Each package build config owns its
|
|
342
|
+
`rootDir`, declaration output, package-specific globals, and publish boundary.
|
|
343
|
+
Projen also places a no-emit config in each `test/` directory so TypeScript 6,
|
|
344
|
+
editors, and type-aware ESLint resolve the nearest project for test files. A
|
|
345
|
+
single config per runtime folder would merge package output boundaries and would
|
|
346
|
+
remove that nearest-config behavior. The runtime tag mixins already centralize
|
|
347
|
+
the environment differences without replacing the package configs.
|
|
348
|
+
|
|
349
|
+
Compilation preserves TypeScript's incremental cache. The root compile task
|
|
350
|
+
passes the package configs to batched `tsc --build` processes, and each config
|
|
351
|
+
retains its own `tsconfig.tsbuildinfo`. Synthesis also retains Projen's native
|
|
352
|
+
change detection: unchanged generated files are not rewritten, and dependency
|
|
353
|
+
installation runs only when the generated root manifest changes or
|
|
354
|
+
`node_modules` is absent. Repository discovery is process-cached so every child
|
|
355
|
+
package reuses the root probe instead of invoking `git`, `ssh`, and `gh` again.
|
|
356
|
+
|
|
341
357
|
## Work With Package Discovery
|
|
342
358
|
|
|
343
359
|
```ts
|
|
@@ -497,6 +513,8 @@ bun run release --publish local --install never
|
|
|
497
513
|
bun run release --local-registry false --local-pypi auto
|
|
498
514
|
bun run release --demo-deploy
|
|
499
515
|
bun run release --no-release-notes
|
|
516
|
+
bun run release --release-notes-text "# Release 1.2.3"
|
|
517
|
+
bun run release --release-notes-file ./notes.md
|
|
500
518
|
bun run release --release-notes-instructions "Focus on operator-visible changes."
|
|
501
519
|
```
|
|
502
520
|
|
|
@@ -512,6 +530,8 @@ After bump and configured validation, release writes
|
|
|
512
530
|
--ephemeral -o`. The standard prompt forbids running tests or other validation
|
|
513
531
|
commands. If Genie fails or writes an empty file, the step keeps going with a
|
|
514
532
|
short git-log summary. `--no-release-notes` skips the file.
|
|
533
|
+
`--release-notes-text` writes supplied markdown directly, while
|
|
534
|
+
`--release-notes-file` copies a prepared markdown file. Both bypass Genie.
|
|
515
535
|
`--release-notes-instructions` appends run-specific guidance to the standard
|
|
516
536
|
Genie prompt. Notes are committed with the version bump and are not recorded in
|
|
517
537
|
the annotated tag. They are skipped on `--no-bump` because that path requires an
|
|
@@ -522,7 +542,8 @@ already committed tree.
|
|
|
522
542
|
installed while still regenerating versioned sources. Local release validation
|
|
523
543
|
runs `eslint:fix` before branch preparation and bump, then runs the selected
|
|
524
544
|
fail-closed tasks; CI remains check-only.
|
|
525
|
-
|
|
545
|
+
CI rebuilds the selected release artifacts instead of checking generated-source fingerprints.
|
|
546
|
+
`--no-validation` skips optional task checks locally and in CI, not workspace-version or immutable-source checks.
|
|
526
547
|
`--no-docs` skips building and deploying the site, not package README validation.
|
|
527
548
|
Scopes select artifacts, not the normal commit and tag-push transaction.
|
|
528
549
|
The annotated tag records CI step selections, so publishing a selected scope does
|
|
@@ -588,8 +609,10 @@ release [options]
|
|
|
588
609
|
| `--no-pypi` | skip Python build and publication, including local Python publication |
|
|
589
610
|
| `--docs` | build and deploy docs for a selected scope |
|
|
590
611
|
| `--no-docs` | skip documentation build and deployment |
|
|
591
|
-
| `--no-validation` | skip optional release validation tasks; version
|
|
612
|
+
| `--no-validation` | skip optional release validation tasks; workspace version checks remain mandatory |
|
|
592
613
|
| `--no-release-notes` | skip writing docs/releases notes (Genie and git-log fallback) |
|
|
614
|
+
| `--release-notes-text <markdown>` | write supplied markdown without invoking Genie |
|
|
615
|
+
| `--release-notes-file <path>` | copy supplied markdown without invoking Genie |
|
|
593
616
|
| `--release-notes-instructions <text>` | append custom instructions to the Genie release-notes prompt |
|
|
594
617
|
| `--demo-deploy` | after tagging, stage and deploy the AppKit demo app (off by default) |
|
|
595
618
|
| `--profile <name>` | Databricks CLI profile for --demo-deploy |
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.99";
|
|
7
7
|
export * as barrels from "./src/barrels.ts";
|
|
8
8
|
export * as bunApp from "./src/bun-app.ts";
|
|
9
9
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
package/package.json
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.7.0",
|
|
33
|
-
"@dbx-tools/core": "^0.9.
|
|
34
|
-
"@dbx-tools/path": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
+
"@dbx-tools/core": "^0.9.99",
|
|
34
|
+
"@dbx-tools/path": "^0.9.99",
|
|
35
|
+
"@dbx-tools/shared-core": "^0.9.99",
|
|
36
36
|
"commander": "^15.0.0",
|
|
37
37
|
"concurrently": "^10.0.3",
|
|
38
38
|
"constructs": "^10.6.0",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"main": "index.ts",
|
|
56
56
|
"license": "Apache-2.0",
|
|
57
|
-
"version": "0.9.
|
|
57
|
+
"version": "0.9.99",
|
|
58
58
|
"types": "index.ts",
|
|
59
59
|
"type": "module",
|
|
60
60
|
"exports": {
|
package/src/project-js.ts
CHANGED
|
@@ -12,7 +12,7 @@ import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
|
12
12
|
import { ignore, match } from "@dbx-tools/path";
|
|
13
13
|
import { object, stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
|
|
14
14
|
import { type IConstruct } from "constructs";
|
|
15
|
-
import { Component, LogLevel, Project, javascript, typescript } from "projen";
|
|
15
|
+
import { Component, LogLevel, Project, type Task, javascript, typescript } from "projen";
|
|
16
16
|
import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
|
|
17
17
|
import type { ReleaseProjectOptions } from "projen/lib/release";
|
|
18
18
|
import { generateBarrels } from "./barrels.ts";
|
|
@@ -587,7 +587,6 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
|
|
|
587
587
|
/** Release build prerequisites installed before repository validation tasks. */
|
|
588
588
|
readonly releaseSetupSteps?: readonly JobStep[];
|
|
589
589
|
/** Repository-specific synthesis commands run before release source verification. */
|
|
590
|
-
readonly releaseSynthesisCommands?: readonly string[];
|
|
591
590
|
/** Set to `false` to omit normal npm workspace publication. */
|
|
592
591
|
readonly nodeRelease?: boolean;
|
|
593
592
|
/** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
|
|
@@ -605,8 +604,9 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
|
|
|
605
604
|
readonly extraWorkspaceMembers?: readonly string[];
|
|
606
605
|
/**
|
|
607
606
|
* Install workspace dependencies once from the custom root instead of once
|
|
608
|
-
* per child project during post-synthesis. Defaults to `true`; set `false`
|
|
609
|
-
* preserve
|
|
607
|
+
* per child project during post-synthesis. Defaults to `true`; set `false`
|
|
608
|
+
* on the root to preserve every native child install, or on an individual
|
|
609
|
+
* child to retain that package's native install lifecycle.
|
|
610
610
|
*/
|
|
611
611
|
readonly rootInstallOnly?: boolean;
|
|
612
612
|
};
|
|
@@ -636,7 +636,7 @@ export class DBXToolsNodeProject
|
|
|
636
636
|
rootTsconfig?: DBXToolsRootTsconfig;
|
|
637
637
|
readonly extraWorkspaceMembers: readonly string[];
|
|
638
638
|
readonly releaseBranch: string;
|
|
639
|
-
|
|
639
|
+
readonly rootInstallOnly: boolean;
|
|
640
640
|
|
|
641
641
|
constructor(options: DBXToolsJavaScriptProjectOptions = {}) {
|
|
642
642
|
validateReleaseOptions(options);
|
|
@@ -684,18 +684,66 @@ export class DBXToolsNodeProject
|
|
|
684
684
|
*
|
|
685
685
|
* Every projen child has its own `NodePackage` post-synth hook, which otherwise
|
|
686
686
|
* runs the install task against the same root workspace once per package. Clear
|
|
687
|
-
* the public child install tasks
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
687
|
+
* the public child install tasks and suppress Projen's protected install hook,
|
|
688
|
+
* while leaving the rest of `NodePackage.postSynthesize` (including dependency
|
|
689
|
+
* resolution) and the root's real install/install:ci tasks intact. A child can
|
|
690
|
+
* set `rootInstallOnly: false` when it owns a package-local dependency tree.
|
|
691
|
+
* Applied in root `preSynthesize` so manually attached late children are
|
|
692
|
+
* included and repeated synths remain idempotent.
|
|
691
693
|
*/
|
|
694
|
+
class RootInstallOnlyChild extends Component {
|
|
695
|
+
private readonly child: DBXToolsNodeProject | DBXToolsTypeScriptProject;
|
|
696
|
+
private readonly installDependencies: (trigger: unknown) => void;
|
|
697
|
+
private suppressed = false;
|
|
698
|
+
|
|
699
|
+
constructor(child: DBXToolsNodeProject | DBXToolsTypeScriptProject) {
|
|
700
|
+
super(child, "RootInstallOnly");
|
|
701
|
+
this.child = child;
|
|
702
|
+
const nodePackage = child.package as unknown as {
|
|
703
|
+
installDependencies(trigger: unknown): void;
|
|
704
|
+
};
|
|
705
|
+
if (typeof nodePackage.installDependencies !== "function") {
|
|
706
|
+
throw new Error("Projen NodePackage no longer exposes installDependencies");
|
|
707
|
+
}
|
|
708
|
+
this.installDependencies = nodePackage.installDependencies;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
public override preSynthesize(): void {
|
|
712
|
+
const nodePackage = this.child.package as unknown as {
|
|
713
|
+
installDependencies(trigger: unknown): void;
|
|
714
|
+
};
|
|
715
|
+
if (
|
|
716
|
+
hasCustomInstallSteps(
|
|
717
|
+
this.child.package.installTask,
|
|
718
|
+
this.child.package.installAndUpdateLockfileCommand,
|
|
719
|
+
) ||
|
|
720
|
+
hasCustomInstallSteps(this.child.package.installCiTask, this.child.package.installCommand)
|
|
721
|
+
) {
|
|
722
|
+
if (this.suppressed) nodePackage.installDependencies = this.installDependencies;
|
|
723
|
+
this.suppressed = false;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.child.package.installTask.reset();
|
|
727
|
+
this.child.package.installCiTask.reset();
|
|
728
|
+
nodePackage.installDependencies = () => {};
|
|
729
|
+
this.suppressed = true;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function hasCustomInstallSteps(task: Task, defaultCommand: string): boolean {
|
|
734
|
+
if (task.steps.length === 0) return false;
|
|
735
|
+
if (task.steps.length !== 1) return true;
|
|
736
|
+
const step = task.steps[0]!;
|
|
737
|
+
return step.exec !== defaultCommand;
|
|
738
|
+
}
|
|
739
|
+
|
|
692
740
|
export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
|
|
693
741
|
(construct: IConstruct): construct is DBXToolsNodeProject | DBXToolsTypeScriptProject =>
|
|
694
742
|
(construct instanceof DBXToolsNodeProject || construct instanceof DBXToolsTypeScriptProject) &&
|
|
695
|
-
construct.parent !== undefined
|
|
743
|
+
construct.parent !== undefined &&
|
|
744
|
+
construct.rootInstallOnly,
|
|
696
745
|
(child) => {
|
|
697
|
-
child.
|
|
698
|
-
child.package.installCiTask.reset();
|
|
746
|
+
if (!child.node.tryFindChild("RootInstallOnly")) new RootInstallOnlyChild(child);
|
|
699
747
|
},
|
|
700
748
|
);
|
|
701
749
|
|
|
@@ -717,7 +765,7 @@ export class DBXToolsTypeScriptProject
|
|
|
717
765
|
rootTsconfig?: DBXToolsRootTsconfig;
|
|
718
766
|
readonly extraWorkspaceMembers: readonly string[];
|
|
719
767
|
readonly releaseBranch: string;
|
|
720
|
-
|
|
768
|
+
readonly rootInstallOnly: boolean;
|
|
721
769
|
|
|
722
770
|
constructor(options: DBXToolsTypeScriptProjectOptions) {
|
|
723
771
|
validateReleaseOptions(options);
|
|
@@ -1331,7 +1379,6 @@ function initProject(
|
|
|
1331
1379
|
pythonRoot: options.releasePythonRoot,
|
|
1332
1380
|
validationTasks: options.releaseValidationTasks,
|
|
1333
1381
|
setupSteps: options.releaseSetupSteps,
|
|
1334
|
-
synthesisCommands: options.releaseSynthesisCommands,
|
|
1335
1382
|
});
|
|
1336
1383
|
}
|
|
1337
1384
|
}
|
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
/** Shared pyproject-backed configuration for PythonMonkey Node bindings. */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { find } from "@dbx-tools/path";
|
|
6
7
|
import { object, stringUtils } from "@dbx-tools/shared-core";
|
|
7
8
|
import { parse } from "smol-toml";
|
|
8
9
|
import ts from "typescript";
|
|
9
10
|
import { runTaskCommand } from "./_task-command.ts";
|
|
11
|
+
import { makeReadonly } from "./generated.ts";
|
|
12
|
+
import { publicFunctionExports, publicNamespaceExports } from "./module-exports.ts";
|
|
10
13
|
import { resolveRepoRoot, workspaceDependencyDirectories } from "./packages.ts";
|
|
11
14
|
|
|
15
|
+
const FINGERPRINT_VERSION = 5;
|
|
16
|
+
const INPUT_FINGERPRINT_LABEL = "dbx-tools binding inputs sha256";
|
|
17
|
+
const CONTENT_FINGERPRINT_LABEL = "dbx-tools binding content sha256";
|
|
18
|
+
const PYTHON_KEYWORDS = new Set([
|
|
19
|
+
"and",
|
|
20
|
+
"as",
|
|
21
|
+
"assert",
|
|
22
|
+
"async",
|
|
23
|
+
"await",
|
|
24
|
+
"break",
|
|
25
|
+
"class",
|
|
26
|
+
"continue",
|
|
27
|
+
"def",
|
|
28
|
+
"del",
|
|
29
|
+
"elif",
|
|
30
|
+
"else",
|
|
31
|
+
"except",
|
|
32
|
+
"False",
|
|
33
|
+
"finally",
|
|
34
|
+
"for",
|
|
35
|
+
"from",
|
|
36
|
+
"global",
|
|
37
|
+
"if",
|
|
38
|
+
"import",
|
|
39
|
+
"in",
|
|
40
|
+
"is",
|
|
41
|
+
"lambda",
|
|
42
|
+
"None",
|
|
43
|
+
"nonlocal",
|
|
44
|
+
"not",
|
|
45
|
+
"or",
|
|
46
|
+
"pass",
|
|
47
|
+
"raise",
|
|
48
|
+
"return",
|
|
49
|
+
"True",
|
|
50
|
+
"try",
|
|
51
|
+
"while",
|
|
52
|
+
"with",
|
|
53
|
+
"yield",
|
|
54
|
+
]);
|
|
55
|
+
|
|
12
56
|
/** Resolved function replacement consumed while generating one PythonMonkey bundle. */
|
|
13
57
|
export interface ResolvedPythonNodeFunctionOverride {
|
|
14
58
|
readonly handlerExport: string;
|
|
@@ -38,6 +82,8 @@ export interface ResolvedPythonNodeBindings {
|
|
|
38
82
|
export interface GeneratePythonNodeBindingsOptions {
|
|
39
83
|
/** Verify generated output without changing it. */
|
|
40
84
|
readonly check?: boolean;
|
|
85
|
+
/** Regenerate outputs without consulting fingerprints. */
|
|
86
|
+
readonly force?: boolean;
|
|
41
87
|
}
|
|
42
88
|
|
|
43
89
|
/** Generate or verify every pyproject-configured Node binding in a repository. */
|
|
@@ -46,10 +92,14 @@ export function generatePythonNodeBindings(
|
|
|
46
92
|
options: GeneratePythonNodeBindingsOptions = {},
|
|
47
93
|
): void {
|
|
48
94
|
const task = resolve(dirname(fileURLToPath(import.meta.url)), "../tasks/python-node-bindings.ts");
|
|
49
|
-
const
|
|
95
|
+
const force = options.force ?? process.env.CI === "true";
|
|
96
|
+
const configuredProjects = pythonNodeBindingProjects(projectRoot).map((project) => ({
|
|
97
|
+
project,
|
|
98
|
+
configs: resolvePythonNodeBindings(projectRoot, project).map(resolvePythonNodeBindingModules),
|
|
99
|
+
}));
|
|
50
100
|
const directories = new Set<string>();
|
|
51
|
-
for (const
|
|
52
|
-
for (const config of
|
|
101
|
+
for (const { configs } of configuredProjects) {
|
|
102
|
+
for (const config of configs) {
|
|
53
103
|
directories.add(relative(projectRoot, join(config.moduleDirectory, "_generated", "node")));
|
|
54
104
|
}
|
|
55
105
|
}
|
|
@@ -64,7 +114,12 @@ export function generatePythonNodeBindings(
|
|
|
64
114
|
for (const directory of stale) {
|
|
65
115
|
rmSync(resolve(projectRoot, directory), { recursive: true, force: true });
|
|
66
116
|
}
|
|
67
|
-
for (const project of configuredProjects) {
|
|
117
|
+
for (const { configs, project } of configuredProjects) {
|
|
118
|
+
const inputFingerprint = pythonNodeBindingInputFingerprint(projectRoot, configs);
|
|
119
|
+
if (!force && pythonNodeBindingOutputsCurrent(configs, inputFingerprint)) {
|
|
120
|
+
for (const output of pythonNodeBindingOutputs(configs)) makeReadonly(output);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
68
123
|
runTaskCommand(projectRoot, "bun", [
|
|
69
124
|
task,
|
|
70
125
|
"--root",
|
|
@@ -72,10 +127,194 @@ export function generatePythonNodeBindings(
|
|
|
72
127
|
"--project",
|
|
73
128
|
project,
|
|
74
129
|
...(options.check ? ["--check"] : []),
|
|
130
|
+
...(force ? ["--force"] : []),
|
|
75
131
|
]);
|
|
76
132
|
}
|
|
77
133
|
}
|
|
78
134
|
|
|
135
|
+
/** Resolve automatically discovered bindable namespaces for one configured package. */
|
|
136
|
+
export function resolvePythonNodeBindingModules(
|
|
137
|
+
config: ResolvedPythonNodeBindings,
|
|
138
|
+
): ResolvedPythonNodeBindings {
|
|
139
|
+
if (config.modules.length > 0) return config;
|
|
140
|
+
const packageEntrypoint = Bun.resolveSync(config.entrypoint, config.projectDirectory);
|
|
141
|
+
const modules = publicNamespaceExports(packageEntrypoint).filter(
|
|
142
|
+
(module) =>
|
|
143
|
+
publicFunctionExports(resolvePythonNodeBindingModule(packageEntrypoint, module)).length > 0,
|
|
144
|
+
);
|
|
145
|
+
return { ...config, modules };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Map a JavaScript export or namespace to its generated Python identifier. */
|
|
149
|
+
export function pythonNodeBindingFunctionName(javascriptName: string): string {
|
|
150
|
+
const name = stringUtils.toIdentifierWithOptions({ delimiter: "_" }, javascriptName);
|
|
151
|
+
if (!isPythonNodeBindingIdentifier(name)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`JavaScript export ${javascriptName} does not map to a valid Python function name`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return name;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Whether a value is a legal non-keyword Python identifier. */
|
|
160
|
+
export function isPythonNodeBindingIdentifier(value: string): boolean {
|
|
161
|
+
return /^[_A-Za-z]\w*$/.test(value) && !PYTHON_KEYWORDS.has(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Content fingerprint for every source and toolchain input affecting generated bindings. */
|
|
165
|
+
export function pythonNodeBindingInputFingerprint(
|
|
166
|
+
root: string,
|
|
167
|
+
configs: readonly ResolvedPythonNodeBindings[],
|
|
168
|
+
): string {
|
|
169
|
+
const canonicalRoot = canonicalPath(root);
|
|
170
|
+
const fingerprintPath = (path: string): string => relative(canonicalRoot, canonicalPath(path));
|
|
171
|
+
const sourceFile = canonicalPath(fileURLToPath(import.meta.url));
|
|
172
|
+
const sourceDirectory = dirname(sourceFile);
|
|
173
|
+
const files = new Set<string>([
|
|
174
|
+
sourceFile,
|
|
175
|
+
canonicalPath(resolve(sourceDirectory, "../tasks/python-node-bindings.ts")),
|
|
176
|
+
canonicalPath(join(sourceDirectory, "generated.ts")),
|
|
177
|
+
canonicalPath(join(sourceDirectory, "module-exports.ts")),
|
|
178
|
+
canonicalPath(join(sourceDirectory, "packages.ts")),
|
|
179
|
+
canonicalPath(join(sourceDirectory, "python-node-runtime.ts")),
|
|
180
|
+
]);
|
|
181
|
+
const versionFile = join(root, "VERSION");
|
|
182
|
+
if (existsSync(versionFile)) files.add(canonicalPath(versionFile));
|
|
183
|
+
for (const config of configs) {
|
|
184
|
+
for (const input of config.watchInputs) files.add(canonicalPath(resolve(root, input)));
|
|
185
|
+
for (const { handlerFile } of config.functionOverrides) files.add(canonicalPath(handlerFile));
|
|
186
|
+
const packageEntrypoint = canonicalPath(
|
|
187
|
+
Bun.resolveSync(config.entrypoint, config.projectDirectory),
|
|
188
|
+
);
|
|
189
|
+
files.add(packageEntrypoint);
|
|
190
|
+
const packageManifest = nearestPackageManifest(packageEntrypoint);
|
|
191
|
+
if (packageManifest) files.add(canonicalPath(packageManifest));
|
|
192
|
+
for (const module of config.modules) {
|
|
193
|
+
files.add(canonicalPath(resolvePythonNodeBindingModule(packageEntrypoint, module)));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const hash = createHash("sha256");
|
|
198
|
+
hash.update(
|
|
199
|
+
JSON.stringify({
|
|
200
|
+
configs: configs.map((config) => ({
|
|
201
|
+
bindingDirectory: fingerprintPath(config.bindingDirectory),
|
|
202
|
+
bindingName: config.bindingName,
|
|
203
|
+
entrypoint: config.entrypoint,
|
|
204
|
+
functionOverrides: config.functionOverrides.map((override) => ({
|
|
205
|
+
handlerExport: override.handlerExport,
|
|
206
|
+
handlerFile: fingerprintPath(override.handlerFile),
|
|
207
|
+
targetExport: override.targetExport,
|
|
208
|
+
targetModule: override.targetModule,
|
|
209
|
+
})),
|
|
210
|
+
moduleDirectory: fingerprintPath(config.moduleDirectory),
|
|
211
|
+
modules: config.modules,
|
|
212
|
+
package: config.package,
|
|
213
|
+
project: config.project,
|
|
214
|
+
runtimeOutput: fingerprintPath(config.runtimeOutput),
|
|
215
|
+
})),
|
|
216
|
+
format: FINGERPRINT_VERSION,
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
for (const { file, path } of [...files]
|
|
220
|
+
.map((file) => ({ file, path: fingerprintPath(file) }))
|
|
221
|
+
.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
222
|
+
hash.update("\0file\0");
|
|
223
|
+
hash.update(path);
|
|
224
|
+
hash.update("\0");
|
|
225
|
+
hash.update(existsSync(file) ? readFileSync(file) : "<missing>");
|
|
226
|
+
}
|
|
227
|
+
return hash.digest("hex");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Complete generated output set for one Python project's Node bindings. */
|
|
231
|
+
export function pythonNodeBindingOutputs(configs: readonly ResolvedPythonNodeBindings[]): string[] {
|
|
232
|
+
const nodeDirectory = pythonNodeBindingDirectory(configs);
|
|
233
|
+
return [
|
|
234
|
+
join(nodeDirectory, "_runtime.js"),
|
|
235
|
+
join(nodeDirectory, "_runtime.py"),
|
|
236
|
+
...configs.flatMap((config) =>
|
|
237
|
+
config.modules.length > 0
|
|
238
|
+
? config.modules.map((module) =>
|
|
239
|
+
join(config.bindingDirectory, `${pythonNodeBindingFunctionName(module)}.py`),
|
|
240
|
+
)
|
|
241
|
+
: [join(config.bindingDirectory, "index.py")],
|
|
242
|
+
),
|
|
243
|
+
].sort();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Whether all expected outputs exactly match their input and body fingerprints. */
|
|
247
|
+
export function pythonNodeBindingOutputsCurrent(
|
|
248
|
+
configs: readonly ResolvedPythonNodeBindings[],
|
|
249
|
+
inputFingerprint: string,
|
|
250
|
+
): boolean {
|
|
251
|
+
const generatedPackage = join(configs[0]!.moduleDirectory, "_generated", "__init__.py");
|
|
252
|
+
if (existsSync(generatedPackage)) return false;
|
|
253
|
+
const expected = pythonNodeBindingOutputs(configs);
|
|
254
|
+
const actual = filesBelow(pythonNodeBindingDirectory(configs)).sort();
|
|
255
|
+
if (actual.length !== expected.length || actual.some((file, index) => file !== expected[index])) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return expected.every((output) => pythonNodeBindingFingerprintCurrent(output, inputFingerprint));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Prefix generated output with its input fingerprint and a self-validating body digest. */
|
|
262
|
+
export function fingerprintPythonNodeBindingOutput(
|
|
263
|
+
contents: string,
|
|
264
|
+
inputFingerprint: string,
|
|
265
|
+
output: string,
|
|
266
|
+
): string {
|
|
267
|
+
const prefix = output.endsWith(".py") ? "#" : "//";
|
|
268
|
+
const contentFingerprint = createHash("sha256").update(contents).digest("hex");
|
|
269
|
+
return [
|
|
270
|
+
`${prefix} ${INPUT_FINGERPRINT_LABEL}: ${inputFingerprint}`,
|
|
271
|
+
`${prefix} ${CONTENT_FINGERPRINT_LABEL}: ${contentFingerprint}`,
|
|
272
|
+
contents,
|
|
273
|
+
].join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function pythonNodeBindingDirectory(configs: readonly ResolvedPythonNodeBindings[]): string {
|
|
277
|
+
const moduleDirectory = configs[0]?.moduleDirectory;
|
|
278
|
+
if (!moduleDirectory) throw new Error("Expected at least one Node binding configuration");
|
|
279
|
+
return join(moduleDirectory, "_generated", "node");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function pythonNodeBindingFingerprintCurrent(output: string, inputFingerprint: string): boolean {
|
|
283
|
+
try {
|
|
284
|
+
const [inputLine, contentLine, ...bodyLines] = readFileSync(output, "utf8").split("\n");
|
|
285
|
+
const prefix = output.endsWith(".py") ? "#" : "//";
|
|
286
|
+
if (inputLine !== `${prefix} ${INPUT_FINGERPRINT_LABEL}: ${inputFingerprint}`) return false;
|
|
287
|
+
const escapedPrefix = prefix === "//" ? "\\/\\/" : "#";
|
|
288
|
+
const expectedContent = contentLine?.match(
|
|
289
|
+
new RegExp(`^${escapedPrefix} ${CONTENT_FINGERPRINT_LABEL}: ([a-f0-9]{64})$`),
|
|
290
|
+
)?.[1];
|
|
291
|
+
if (!expectedContent) return false;
|
|
292
|
+
return createHash("sha256").update(bodyLines.join("\n")).digest("hex") === expectedContent;
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function filesBelow(directory: string): string[] {
|
|
299
|
+
if (!existsSync(directory)) return [];
|
|
300
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
301
|
+
if (entry.name === "__pycache__" || entry.name.endsWith(".pyc")) return [];
|
|
302
|
+
const path = join(directory, entry.name);
|
|
303
|
+
return entry.isDirectory() ? filesBelow(path) : [path];
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function nearestPackageManifest(path: string): string | undefined {
|
|
308
|
+
let directory = dirname(path);
|
|
309
|
+
while (true) {
|
|
310
|
+
const manifest = join(directory, "package.json");
|
|
311
|
+
if (existsSync(manifest)) return manifest;
|
|
312
|
+
const parent = dirname(directory);
|
|
313
|
+
if (parent === directory) return undefined;
|
|
314
|
+
directory = parent;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
79
318
|
/** Resolve every configured binding in a repository. */
|
|
80
319
|
export function resolveAllPythonNodeBindings(
|
|
81
320
|
projectRoot: string = resolveRepoRoot(),
|
|
@@ -102,16 +341,28 @@ export function pythonNodeBindingProjects(projectRoot: string): string[] {
|
|
|
102
341
|
|
|
103
342
|
function existingBindingDirectories(projectRoot: string): string[] {
|
|
104
343
|
const directories = new Set<string>();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
344
|
+
const ignored = new Set([
|
|
345
|
+
".git",
|
|
346
|
+
".mypy_cache",
|
|
347
|
+
".pytest_cache",
|
|
348
|
+
".ruff_cache",
|
|
349
|
+
".venv",
|
|
350
|
+
"__pycache__",
|
|
351
|
+
"dist",
|
|
352
|
+
"node_modules",
|
|
353
|
+
]);
|
|
354
|
+
const visit = (directory: string): void => {
|
|
355
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
356
|
+
if (!entry.isDirectory() || ignored.has(entry.name)) continue;
|
|
357
|
+
const path = join(directory, entry.name);
|
|
358
|
+
if (entry.name === "node" && basename(directory) === "_generated") {
|
|
359
|
+
directories.add(relative(projectRoot, path));
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
visit(path);
|
|
113
363
|
}
|
|
114
|
-
}
|
|
364
|
+
};
|
|
365
|
+
visit(projectRoot);
|
|
115
366
|
return [...directories].sort();
|
|
116
367
|
}
|
|
117
368
|
|
|
@@ -192,16 +443,23 @@ type BindingSourceConfig = Omit<ResolvedPythonNodeBindings, "watchInputs">;
|
|
|
192
443
|
|
|
193
444
|
/** Transitive workspace source files reachable from configured binding modules. */
|
|
194
445
|
function bindingSourceInputs(root: string, config: BindingSourceConfig): string[] {
|
|
195
|
-
const allowed = config.workspaceDirectories.map(canonicalPath);
|
|
196
446
|
const packageEntrypoint = Bun.resolveSync(config.entrypoint, config.projectDirectory);
|
|
197
447
|
const entrypoints =
|
|
198
448
|
config.modules.length > 0
|
|
199
449
|
? config.modules.map((module) => resolvePythonNodeBindingModule(packageEntrypoint, module))
|
|
200
450
|
: [packageEntrypoint];
|
|
201
|
-
|
|
202
|
-
...entrypoints,
|
|
203
|
-
|
|
204
|
-
|
|
451
|
+
return transitiveSourceFiles(
|
|
452
|
+
[...entrypoints, ...config.functionOverrides.map(({ handlerFile }) => handlerFile)],
|
|
453
|
+
config.workspaceDirectories,
|
|
454
|
+
).map((path) => relative(root, path));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function transitiveSourceFiles(
|
|
458
|
+
entrypoints: readonly string[],
|
|
459
|
+
allowedDirectories: readonly string[],
|
|
460
|
+
): string[] {
|
|
461
|
+
const allowed = allowedDirectories.map(canonicalPath);
|
|
462
|
+
const pending = [...entrypoints];
|
|
205
463
|
const visited = new Set<string>();
|
|
206
464
|
while (pending.length > 0) {
|
|
207
465
|
const candidate = canonicalPath(pending.pop()!);
|
|
@@ -227,7 +485,7 @@ function bindingSourceInputs(root: string, config: BindingSourceConfig): string[
|
|
|
227
485
|
}
|
|
228
486
|
}
|
|
229
487
|
}
|
|
230
|
-
return [...visited].sort()
|
|
488
|
+
return [...visited].sort();
|
|
231
489
|
}
|
|
232
490
|
|
|
233
491
|
/** Resolve one configured namespace from a package entrypoint barrel. */
|
package/src/release.ts
CHANGED
|
@@ -51,8 +51,6 @@ export interface DBXToolsReleaseOptions {
|
|
|
51
51
|
readonly validationTasks?: readonly string[];
|
|
52
52
|
/** Repository prerequisites installed before release validation and artifact builds. */
|
|
53
53
|
readonly setupSteps?: readonly JobStep[];
|
|
54
|
-
/** Repository-specific synthesis commands run before the root Projen synthesis check. */
|
|
55
|
-
readonly synthesisCommands?: readonly string[];
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
/** Locate the unified workflow when release generation is enabled. */
|
|
@@ -204,14 +202,6 @@ function releaseBuildJob(
|
|
|
204
202
|
// ============================================================================
|
|
205
203
|
),
|
|
206
204
|
},
|
|
207
|
-
{
|
|
208
|
-
name: "Verify generated sources",
|
|
209
|
-
run: [
|
|
210
|
-
...(options.synthesisCommands ?? []),
|
|
211
|
-
"bunx projen",
|
|
212
|
-
"git diff --ignore-space-at-eol --exit-code",
|
|
213
|
-
].join("\n"),
|
|
214
|
-
},
|
|
215
205
|
{ name: "Verify workspace versions", run: "bun run version:check" },
|
|
216
206
|
...(options.setupSteps ?? []),
|
|
217
207
|
...releaseBuildSteps(
|
package/src/tsconfig.ts
CHANGED
|
@@ -55,7 +55,7 @@ export class DBXToolsRootTsconfig extends Component {
|
|
|
55
55
|
extends: javascript.TypescriptConfigExtends.fromTypescriptConfigs([this.base]),
|
|
56
56
|
compilerOptions: {
|
|
57
57
|
lib: ["ESNext"],
|
|
58
|
-
types: ["node"],
|
|
58
|
+
types: ["node", "bun"],
|
|
59
59
|
},
|
|
60
60
|
include: [".projenrc.ts"],
|
|
61
61
|
exclude: ["node_modules", "**/dist", "**/node_modules"],
|
|
@@ -12,11 +12,18 @@ import type { BunPlugin } from "bun";
|
|
|
12
12
|
import stdLibBrowser from "node-stdlib-browser";
|
|
13
13
|
import ts from "typescript";
|
|
14
14
|
import { header, makeReadonly, makeWritable } from "../src/generated.ts";
|
|
15
|
-
import { publicFunctionExports
|
|
15
|
+
import { publicFunctionExports } from "../src/module-exports.ts";
|
|
16
16
|
import { resolveRepoRoot } from "../src/packages.ts";
|
|
17
17
|
import {
|
|
18
18
|
generatePythonNodeBindings,
|
|
19
|
+
fingerprintPythonNodeBindingOutput,
|
|
20
|
+
isPythonNodeBindingIdentifier,
|
|
21
|
+
pythonNodeBindingFunctionName as pythonFunctionName,
|
|
22
|
+
pythonNodeBindingInputFingerprint,
|
|
23
|
+
pythonNodeBindingOutputs,
|
|
24
|
+
pythonNodeBindingOutputsCurrent,
|
|
19
25
|
resolvePythonNodeBindingModule,
|
|
26
|
+
resolvePythonNodeBindingModules,
|
|
20
27
|
resolvePythonNodeBindings,
|
|
21
28
|
type ResolvedPythonNodeBindings,
|
|
22
29
|
type ResolvedPythonNodeFunctionOverride,
|
|
@@ -35,50 +42,13 @@ function bindingIndex(value: string, count: number, project: string): number {
|
|
|
35
42
|
return index;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
const PYTHON_KEYWORDS = new Set([
|
|
39
|
-
"and",
|
|
40
|
-
"as",
|
|
41
|
-
"assert",
|
|
42
|
-
"async",
|
|
43
|
-
"await",
|
|
44
|
-
"break",
|
|
45
|
-
"class",
|
|
46
|
-
"continue",
|
|
47
|
-
"def",
|
|
48
|
-
"del",
|
|
49
|
-
"elif",
|
|
50
|
-
"else",
|
|
51
|
-
"except",
|
|
52
|
-
"False",
|
|
53
|
-
"finally",
|
|
54
|
-
"for",
|
|
55
|
-
"from",
|
|
56
|
-
"global",
|
|
57
|
-
"if",
|
|
58
|
-
"import",
|
|
59
|
-
"in",
|
|
60
|
-
"is",
|
|
61
|
-
"lambda",
|
|
62
|
-
"None",
|
|
63
|
-
"nonlocal",
|
|
64
|
-
"not",
|
|
65
|
-
"or",
|
|
66
|
-
"pass",
|
|
67
|
-
"raise",
|
|
68
|
-
"return",
|
|
69
|
-
"True",
|
|
70
|
-
"try",
|
|
71
|
-
"while",
|
|
72
|
-
"with",
|
|
73
|
-
"yield",
|
|
74
|
-
]);
|
|
75
|
-
|
|
76
45
|
export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
|
|
77
46
|
const { values } = parseArgs({
|
|
78
47
|
args,
|
|
79
48
|
options: {
|
|
80
49
|
binding: { type: "string" },
|
|
81
50
|
check: { type: "boolean" },
|
|
51
|
+
force: { type: "boolean" },
|
|
82
52
|
module: { type: "string" },
|
|
83
53
|
project: { type: "string" },
|
|
84
54
|
root: { type: "string" },
|
|
@@ -89,11 +59,13 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
89
59
|
if (values.binding !== undefined || values.module !== undefined) {
|
|
90
60
|
throw new Error("--binding and --module require --project");
|
|
91
61
|
}
|
|
92
|
-
generatePythonNodeBindings(root, { check: values.check });
|
|
62
|
+
generatePythonNodeBindings(root, { check: values.check, force: values.force });
|
|
93
63
|
return;
|
|
94
64
|
}
|
|
95
65
|
|
|
96
|
-
const configs = resolvePythonNodeBindings(root, values.project).map(
|
|
66
|
+
const configs = resolvePythonNodeBindings(root, values.project).map(
|
|
67
|
+
resolvePythonNodeBindingModules,
|
|
68
|
+
);
|
|
97
69
|
if (configs.length > 1 && configs.some(({ modules }) => modules.length === 0)) {
|
|
98
70
|
throw new Error(
|
|
99
71
|
"Every Node binding must export a namespace with bindable functions or configure modules",
|
|
@@ -105,6 +77,18 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
105
77
|
: bindingIndex(values.binding, configs.length, values.project);
|
|
106
78
|
const config = selectedBinding === undefined ? configs[0] : configs[selectedBinding];
|
|
107
79
|
if (!config) throw new Error("Expected at least one Node binding configuration");
|
|
80
|
+
const inputFingerprint = pythonNodeBindingInputFingerprint(root, configs);
|
|
81
|
+
if (
|
|
82
|
+
selectedBinding === undefined &&
|
|
83
|
+
!values.force &&
|
|
84
|
+
pythonNodeBindingOutputsCurrent(configs, inputFingerprint)
|
|
85
|
+
) {
|
|
86
|
+
for (const output of pythonNodeBindingOutputs(configs)) makeReadonly(output);
|
|
87
|
+
if (values.check) {
|
|
88
|
+
logger.info(`verified ${relative(root, dirname(config.runtimeOutput))} from fingerprints`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
108
92
|
if (selectedBinding === undefined && (configs.length > 1 || config.modules.length > 0)) {
|
|
109
93
|
prepareBindingDirectory(configs);
|
|
110
94
|
writeRuntimeLoader(configs);
|
|
@@ -115,6 +99,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
115
99
|
values.project,
|
|
116
100
|
...(values.root ? ["--root", values.root] : []),
|
|
117
101
|
...(values.check ? ["--check"] : []),
|
|
102
|
+
...(values.force ? ["--force"] : []),
|
|
118
103
|
"--binding",
|
|
119
104
|
String(binding),
|
|
120
105
|
"--module",
|
|
@@ -217,9 +202,9 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
217
202
|
const entrypoint = Bun.resolveSync(binding.entrypoint, binding.projectDirectory);
|
|
218
203
|
return binding.modules.length > 0
|
|
219
204
|
? binding.modules.map((module) => ({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
205
|
+
key: runtimeModuleName(binding, module),
|
|
206
|
+
entrypoint: resolvePythonNodeBindingModule(entrypoint, module),
|
|
207
|
+
}))
|
|
223
208
|
: [{ key: runtimeModuleName(binding), entrypoint }];
|
|
224
209
|
});
|
|
225
210
|
const runtimeExports = [
|
|
@@ -304,18 +289,6 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
304
289
|
}
|
|
305
290
|
await generate();
|
|
306
291
|
|
|
307
|
-
function discoverBindingModules(
|
|
308
|
-
candidate: ResolvedPythonNodeBindings,
|
|
309
|
-
): ResolvedPythonNodeBindings {
|
|
310
|
-
if (candidate.modules.length > 0) return candidate;
|
|
311
|
-
const packageEntrypoint = Bun.resolveSync(candidate.entrypoint, candidate.projectDirectory);
|
|
312
|
-
const modules = publicNamespaceExports(packageEntrypoint).filter(
|
|
313
|
-
(module) =>
|
|
314
|
-
publicFunctionExports(resolvePythonNodeBindingModule(packageEntrypoint, module)).length > 0,
|
|
315
|
-
);
|
|
316
|
-
return { ...candidate, modules };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
292
|
async function generate(): Promise<void> {
|
|
320
293
|
const result = await Bun.build({
|
|
321
294
|
entrypoints: [shimEntry],
|
|
@@ -367,8 +340,8 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
367
340
|
...bindings.flatMap((binding) => [
|
|
368
341
|
...(binding.modules.length > 0
|
|
369
342
|
? binding.modules.map((module) =>
|
|
370
|
-
|
|
371
|
-
|
|
343
|
+
join(binding.bindingDirectory, `${pythonFunctionName(module)}.py`),
|
|
344
|
+
)
|
|
372
345
|
: [join(binding.bindingDirectory, "index.py")]),
|
|
373
346
|
]),
|
|
374
347
|
].filter((file): file is string => Boolean(file)),
|
|
@@ -414,16 +387,6 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
414
387
|
return grouped;
|
|
415
388
|
}
|
|
416
389
|
|
|
417
|
-
function pythonFunctionName(javascriptName: string): string {
|
|
418
|
-
const name = stringUtils.toIdentifierWithOptions({ delimiter: "_" }, javascriptName);
|
|
419
|
-
if (!/^[_A-Za-z]\w*$/.test(name) || PYTHON_KEYWORDS.has(name)) {
|
|
420
|
-
throw new Error(
|
|
421
|
-
`JavaScript export ${javascriptName} does not map to a valid Python function name`,
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
return name;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
390
|
function runtimeModuleName(binding: ResolvedPythonNodeBindings, module?: string): string {
|
|
428
391
|
return [binding.bindingName, ...(module ? [pythonFunctionName(module)] : [])].join("__");
|
|
429
392
|
}
|
|
@@ -987,18 +950,18 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
987
950
|
function pythonDataclass(record: PythonRecord): string {
|
|
988
951
|
const body = record.fields.length
|
|
989
952
|
? record.fields
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
953
|
+
.map((field) => {
|
|
954
|
+
const defaultArgument = Object.hasOwn(field, "defaultValue")
|
|
955
|
+
? pythonDefault(field.defaultValue)
|
|
956
|
+
: "default=None";
|
|
957
|
+
return [
|
|
958
|
+
` ${field.pythonName}: ${pythonOptionalType(field.type)} = field(`,
|
|
959
|
+
` ${defaultArgument},`,
|
|
960
|
+
` metadata={"javascript_name": ${JSON.stringify(field.javascriptName)}},`,
|
|
961
|
+
" )",
|
|
962
|
+
].join("\n");
|
|
963
|
+
})
|
|
964
|
+
.join("\n")
|
|
1002
965
|
: " pass";
|
|
1003
966
|
return `@dataclass(kw_only=True)\nclass ${record.name}:\n${body}`;
|
|
1004
967
|
}
|
|
@@ -1026,17 +989,17 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
1026
989
|
}
|
|
1027
990
|
const body = response.fields.length
|
|
1028
991
|
? response.fields
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
992
|
+
.map(
|
|
993
|
+
({ name, required, type }) =>
|
|
994
|
+
` ${name}: ${required ? type : `NotRequired[${type}]`}`,
|
|
995
|
+
)
|
|
996
|
+
.join("\n")
|
|
1034
997
|
: " pass";
|
|
1035
998
|
return `class ${response.name}(TypedDict):\n${body}`;
|
|
1036
999
|
}
|
|
1037
1000
|
|
|
1038
1001
|
function isPythonIdentifier(value: string): boolean {
|
|
1039
|
-
return
|
|
1002
|
+
return isPythonNodeBindingIdentifier(value);
|
|
1040
1003
|
}
|
|
1041
1004
|
|
|
1042
1005
|
function pythonProtocol(protocol: PythonProtocol): string {
|
|
@@ -1045,18 +1008,18 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
1045
1008
|
);
|
|
1046
1009
|
const methods = protocol.methods.length
|
|
1047
1010
|
? protocol.methods
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1011
|
+
.map((method) => {
|
|
1012
|
+
const parameters = method.parameters.map(
|
|
1013
|
+
({ name, required, type }) => ` ${name}: ${type}${required ? "" : " = ..."},`,
|
|
1014
|
+
);
|
|
1015
|
+
return [
|
|
1016
|
+
` async def ${method.name}(`,
|
|
1017
|
+
" self,",
|
|
1018
|
+
...parameters,
|
|
1019
|
+
` ) -> ${method.returnType}: ...`,
|
|
1020
|
+
].join("\n");
|
|
1021
|
+
})
|
|
1022
|
+
.join("\n\n")
|
|
1060
1023
|
: "";
|
|
1061
1024
|
const body = [...properties, methods].filter(Boolean).join("\n\n") || " pass";
|
|
1062
1025
|
return `class ${protocol.name}(Protocol):\n${body}`;
|
|
@@ -1149,7 +1112,8 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
1149
1112
|
|
|
1150
1113
|
function writeGenerated(output: string, contents: string, kind: string): void {
|
|
1151
1114
|
const destination = relative(root, output);
|
|
1152
|
-
|
|
1115
|
+
const generated = fingerprintPythonNodeBindingOutput(contents, inputFingerprint, output);
|
|
1116
|
+
if (existsSync(output) && readFileSync(output, "utf8") === generated) {
|
|
1153
1117
|
if (values.check) logger.info(`verified ${destination}`);
|
|
1154
1118
|
return;
|
|
1155
1119
|
}
|
|
@@ -1158,7 +1122,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
1158
1122
|
}
|
|
1159
1123
|
mkdirSync(dirname(output), { recursive: true });
|
|
1160
1124
|
makeWritable(output);
|
|
1161
|
-
writeFileSync(output,
|
|
1125
|
+
writeFileSync(output, generated);
|
|
1162
1126
|
makeReadonly(output);
|
|
1163
1127
|
logger.info(`generated ${destination}`);
|
|
1164
1128
|
}
|
package/tasks/release-notes.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { dirname, join } from "node:path";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { log } from "@dbx-tools/shared-core";
|
|
13
13
|
|
|
14
14
|
import { captureTaskCommand, tryTaskCommand } from "../src/_task-command.ts";
|
|
@@ -20,6 +20,10 @@ const GENIE_PROMPT =
|
|
|
20
20
|
|
|
21
21
|
/** Inputs for one notes write. */
|
|
22
22
|
export interface WriteReleaseNotesOptions {
|
|
23
|
+
/** Complete release-note content supplied by the caller instead of Genie. */
|
|
24
|
+
readonly content?: string;
|
|
25
|
+
/** File containing complete release notes supplied by the caller instead of Genie. */
|
|
26
|
+
readonly file?: string;
|
|
23
27
|
/** Extra agent instructions appended after the standard release-note requirements. */
|
|
24
28
|
readonly instructions?: string;
|
|
25
29
|
readonly prefix: string;
|
|
@@ -60,6 +64,25 @@ export function writeReleaseNotes(options: WriteReleaseNotesOptions): string {
|
|
|
60
64
|
const destination = releaseNotesPath(root, version);
|
|
61
65
|
mkdirSync(dirname(destination), { recursive: true });
|
|
62
66
|
const relative = join("docs", "releases", `v${version}.md`);
|
|
67
|
+
const suppliedContent = options.content?.trim();
|
|
68
|
+
const suppliedFile = options.file?.trim();
|
|
69
|
+
if (suppliedContent && suppliedFile) {
|
|
70
|
+
throw new Error("release notes content and file are mutually exclusive");
|
|
71
|
+
}
|
|
72
|
+
if (options.content !== undefined) {
|
|
73
|
+
if (!suppliedContent) throw new Error("release notes content must not be empty");
|
|
74
|
+
writeFileSync(destination, `${suppliedContent}\n`);
|
|
75
|
+
logger.info("wrote supplied release notes", { path: relative });
|
|
76
|
+
return destination;
|
|
77
|
+
}
|
|
78
|
+
if (options.file !== undefined) {
|
|
79
|
+
if (!suppliedFile) throw new Error("release notes file must not be empty");
|
|
80
|
+
const content = readNotes(resolve(root, suppliedFile));
|
|
81
|
+
if (!content) throw new Error(`release notes file is empty or unreadable: ${suppliedFile}`);
|
|
82
|
+
writeFileSync(destination, `${content}\n`);
|
|
83
|
+
logger.info("copied supplied release notes", { path: relative, source: suppliedFile });
|
|
84
|
+
return destination;
|
|
85
|
+
}
|
|
63
86
|
const genieArgs = [
|
|
64
87
|
"exec",
|
|
65
88
|
"-C",
|
package/tasks/release.ts
CHANGED
|
@@ -236,6 +236,10 @@ export async function runRelease(
|
|
|
236
236
|
readonly releaseNotes?: boolean;
|
|
237
237
|
/** Additional instructions appended to the standard Genie release-notes prompt. */
|
|
238
238
|
readonly releaseNotesInstructions?: string;
|
|
239
|
+
/** Complete release notes supplied directly instead of invoking Genie. */
|
|
240
|
+
readonly releaseNotesText?: string;
|
|
241
|
+
/** Path to complete release notes supplied instead of invoking Genie. */
|
|
242
|
+
readonly releaseNotesFile?: string;
|
|
239
243
|
readonly writeReleaseNotes?: typeof writeReleaseNotes;
|
|
240
244
|
},
|
|
241
245
|
): Promise<string> {
|
|
@@ -247,6 +251,23 @@ export async function runRelease(
|
|
|
247
251
|
if (options.demoDeploy && !demoProfile) {
|
|
248
252
|
throw new Error("--demo-deploy requires --profile <name>");
|
|
249
253
|
}
|
|
254
|
+
if (options.releaseNotesText !== undefined && options.releaseNotesFile !== undefined) {
|
|
255
|
+
throw new Error("--release-notes-text and --release-notes-file are mutually exclusive");
|
|
256
|
+
}
|
|
257
|
+
if (
|
|
258
|
+
options.releaseNotesInstructions !== undefined &&
|
|
259
|
+
(options.releaseNotesText !== undefined || options.releaseNotesFile !== undefined)
|
|
260
|
+
) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
"--release-notes-instructions cannot be combined with supplied release notes",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
options.releaseNotes === false &&
|
|
267
|
+
(options.releaseNotesText !== undefined || options.releaseNotesFile !== undefined)
|
|
268
|
+
) {
|
|
269
|
+
throw new Error("--no-release-notes cannot be combined with supplied release notes");
|
|
270
|
+
}
|
|
250
271
|
|
|
251
272
|
if (install === "always") runTaskCommand(root, "bun", ["install"]);
|
|
252
273
|
if (selection.validation && hasPackageScript(root, "eslint:fix")) {
|
|
@@ -279,6 +300,8 @@ export async function runRelease(
|
|
|
279
300
|
prefix,
|
|
280
301
|
root,
|
|
281
302
|
version,
|
|
303
|
+
content: options.releaseNotesText,
|
|
304
|
+
file: options.releaseNotesFile,
|
|
282
305
|
instructions: options.releaseNotesInstructions,
|
|
283
306
|
});
|
|
284
307
|
}
|
|
@@ -380,9 +403,11 @@ export function createReleaseCommand(): Command {
|
|
|
380
403
|
.option("--no-docs", "skip documentation build and deployment")
|
|
381
404
|
.option(
|
|
382
405
|
"--no-validation",
|
|
383
|
-
"skip optional release validation tasks; version
|
|
406
|
+
"skip optional release validation tasks; workspace version checks remain mandatory",
|
|
384
407
|
)
|
|
385
408
|
.option("--no-release-notes", "skip writing docs/releases notes (Genie and git-log fallback)")
|
|
409
|
+
.option("--release-notes-text <markdown>", "write supplied markdown without invoking Genie")
|
|
410
|
+
.option("--release-notes-file <path>", "copy supplied markdown without invoking Genie")
|
|
386
411
|
.option(
|
|
387
412
|
"--release-notes-instructions <text>",
|
|
388
413
|
"append custom instructions to the Genie release-notes prompt",
|
|
@@ -410,6 +435,8 @@ export function createReleaseCommand(): Command {
|
|
|
410
435
|
profile?: string;
|
|
411
436
|
releaseNotes?: boolean;
|
|
412
437
|
releaseNotesInstructions?: string;
|
|
438
|
+
releaseNotesText?: string;
|
|
439
|
+
releaseNotesFile?: string;
|
|
413
440
|
},
|
|
414
441
|
) => {
|
|
415
442
|
await runRelease({
|
|
@@ -433,6 +460,8 @@ export function createReleaseCommand(): Command {
|
|
|
433
460
|
demoProfile: options.profile,
|
|
434
461
|
releaseNotes: options.releaseNotes,
|
|
435
462
|
releaseNotesInstructions: options.releaseNotesInstructions,
|
|
463
|
+
releaseNotesText: options.releaseNotesText,
|
|
464
|
+
releaseNotesFile: options.releaseNotesFile,
|
|
436
465
|
});
|
|
437
466
|
},
|
|
438
467
|
);
|