@dbx-tools/projen 0.9.95 → 0.9.105
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 +32 -19
- package/index.ts +1 -1
- package/package.json +4 -4
- package/src/project-js.ts +62 -14
- package/src/python-node-bindings.ts +290 -25
- package/src/release.ts +0 -10
- package/src/tsconfig.ts +1 -1
- package/tasks/bump.ts +5 -0
- package/tasks/python-node-bindings.ts +64 -100
- package/tasks/release-notes.ts +24 -1
- package/tasks/release.ts +28 -1
package/README.md
CHANGED
|
@@ -2,23 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Projen engine for Bun-first dbx-tools workspaces.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
barrels, codegen outputs, and release tasks.
|
|
8
|
-
|
|
9
|
-
Key features:
|
|
10
|
-
|
|
11
|
-
- Filesystem package discovery: every `src`-bearing folder under configured
|
|
12
|
-
workspace roots becomes a TypeScript package.
|
|
13
|
-
- Tag-driven runtime defaults for shared libraries, Node packages, CLIs,
|
|
14
|
-
servers, and React/browser UI packages.
|
|
15
|
-
- Generated package manifests, tsconfigs, package-root barrels, Bun app configs,
|
|
16
|
-
VS Code settings, and a committed `pnpm-workspace.yaml` retained for
|
|
17
|
-
Databricks Apps deployment.
|
|
18
|
-
- Extensible mixin system so repositories can add deps, tasks, or generated
|
|
19
|
-
files based on package predicates.
|
|
20
|
-
- Zod schema generation from `.d.ts` inputs.
|
|
21
|
-
- Read-only generated-file ownership, cleanup, and watch-loop helpers.
|
|
5
|
+
Use it from `.projenrc.ts` to discover packages from the filesystem and generate
|
|
6
|
+
manifests, tsconfigs, barrels, code generation, and release tasks.
|
|
22
7
|
|
|
23
8
|
## Define A Workspace Root
|
|
24
9
|
|
|
@@ -34,6 +19,11 @@ const rootProject = new project.DBXToolsNodeProject({
|
|
|
34
19
|
rootProject.synth();
|
|
35
20
|
```
|
|
36
21
|
|
|
22
|
+
The project discovers every `src`-bearing folder under the configured roots and
|
|
23
|
+
applies defaults for shared libraries, Node packages, CLIs, servers, and React
|
|
24
|
+
packages. It can also generate Zod schemas from declarations and coordinate
|
|
25
|
+
generated-file cleanup and watch tasks.
|
|
26
|
+
|
|
37
27
|
Every `src`-bearing folder under the configured roots becomes a
|
|
38
28
|
`DBXToolsTypeScriptProject`. Folder path drives package name and runtime tags.
|
|
39
29
|
|
|
@@ -338,6 +328,22 @@ wrong fix, since the consumer authors no JSX and cannot know a transitive
|
|
|
338
328
|
dependency started to. The option is inert without JSX in the graph: it selects
|
|
339
329
|
how JSX syntax compiles and adds no lib, global, or type dependency.
|
|
340
330
|
|
|
331
|
+
Keep the generated tsconfigs package-local. Each package build config owns its
|
|
332
|
+
`rootDir`, declaration output, package-specific globals, and publish boundary.
|
|
333
|
+
Projen also places a no-emit config in each `test/` directory so TypeScript 6,
|
|
334
|
+
editors, and type-aware ESLint resolve the nearest project for test files. A
|
|
335
|
+
single config per runtime folder would merge package output boundaries and would
|
|
336
|
+
remove that nearest-config behavior. The runtime tag mixins already centralize
|
|
337
|
+
the environment differences without replacing the package configs.
|
|
338
|
+
|
|
339
|
+
Compilation preserves TypeScript's incremental cache. The root compile task
|
|
340
|
+
passes the package configs to batched `tsc --build` processes, and each config
|
|
341
|
+
retains its own `tsconfig.tsbuildinfo`. Synthesis also retains Projen's native
|
|
342
|
+
change detection: unchanged generated files are not rewritten, and dependency
|
|
343
|
+
installation runs only when the generated root manifest changes or
|
|
344
|
+
`node_modules` is absent. Repository discovery is process-cached so every child
|
|
345
|
+
package reuses the root probe instead of invoking `git`, `ssh`, and `gh` again.
|
|
346
|
+
|
|
341
347
|
## Work With Package Discovery
|
|
342
348
|
|
|
343
349
|
```ts
|
|
@@ -497,6 +503,8 @@ bun run release --publish local --install never
|
|
|
497
503
|
bun run release --local-registry false --local-pypi auto
|
|
498
504
|
bun run release --demo-deploy
|
|
499
505
|
bun run release --no-release-notes
|
|
506
|
+
bun run release --release-notes-text "# Release 1.2.3"
|
|
507
|
+
bun run release --release-notes-file ./notes.md
|
|
500
508
|
bun run release --release-notes-instructions "Focus on operator-visible changes."
|
|
501
509
|
```
|
|
502
510
|
|
|
@@ -512,6 +520,8 @@ After bump and configured validation, release writes
|
|
|
512
520
|
--ephemeral -o`. The standard prompt forbids running tests or other validation
|
|
513
521
|
commands. If Genie fails or writes an empty file, the step keeps going with a
|
|
514
522
|
short git-log summary. `--no-release-notes` skips the file.
|
|
523
|
+
`--release-notes-text` writes supplied markdown directly, while
|
|
524
|
+
`--release-notes-file` copies a prepared markdown file. Both bypass Genie.
|
|
515
525
|
`--release-notes-instructions` appends run-specific guidance to the standard
|
|
516
526
|
Genie prompt. Notes are committed with the version bump and are not recorded in
|
|
517
527
|
the annotated tag. They are skipped on `--no-bump` because that path requires an
|
|
@@ -522,7 +532,8 @@ already committed tree.
|
|
|
522
532
|
installed while still regenerating versioned sources. Local release validation
|
|
523
533
|
runs `eslint:fix` before branch preparation and bump, then runs the selected
|
|
524
534
|
fail-closed tasks; CI remains check-only.
|
|
525
|
-
|
|
535
|
+
CI rebuilds the selected release artifacts instead of checking generated-source fingerprints.
|
|
536
|
+
`--no-validation` skips optional task checks locally and in CI, not workspace-version or immutable-source checks.
|
|
526
537
|
`--no-docs` skips building and deploying the site, not package README validation.
|
|
527
538
|
Scopes select artifacts, not the normal commit and tag-push transaction.
|
|
528
539
|
The annotated tag records CI step selections, so publishing a selected scope does
|
|
@@ -588,8 +599,10 @@ release [options]
|
|
|
588
599
|
| `--no-pypi` | skip Python build and publication, including local Python publication |
|
|
589
600
|
| `--docs` | build and deploy docs for a selected scope |
|
|
590
601
|
| `--no-docs` | skip documentation build and deployment |
|
|
591
|
-
| `--no-validation` | skip optional release validation tasks; version
|
|
602
|
+
| `--no-validation` | skip optional release validation tasks; workspace version checks remain mandatory |
|
|
592
603
|
| `--no-release-notes` | skip writing docs/releases notes (Genie and git-log fallback) |
|
|
604
|
+
| `--release-notes-text <markdown>` | write supplied markdown without invoking Genie |
|
|
605
|
+
| `--release-notes-file <path>` | copy supplied markdown without invoking Genie |
|
|
593
606
|
| `--release-notes-instructions <text>` | append custom instructions to the Genie release-notes prompt |
|
|
594
607
|
| `--demo-deploy` | after tagging, stage and deploy the AppKit demo app (off by default) |
|
|
595
608
|
| `--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.105";
|
|
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.105",
|
|
34
|
+
"@dbx-tools/path": "^0.9.105",
|
|
35
|
+
"@dbx-tools/shared-core": "^0.9.105",
|
|
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.105",
|
|
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,67 @@ 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
|
+
|
|
740
|
+
/** Suppress redundant child install tasks when the workspace root owns dependency installation. */
|
|
692
741
|
export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
|
|
693
742
|
(construct: IConstruct): construct is DBXToolsNodeProject | DBXToolsTypeScriptProject =>
|
|
694
743
|
(construct instanceof DBXToolsNodeProject || construct instanceof DBXToolsTypeScriptProject) &&
|
|
695
|
-
construct.parent !== undefined
|
|
744
|
+
construct.parent !== undefined &&
|
|
745
|
+
construct.rootInstallOnly,
|
|
696
746
|
(child) => {
|
|
697
|
-
child.
|
|
698
|
-
child.package.installCiTask.reset();
|
|
747
|
+
if (!child.node.tryFindChild("RootInstallOnly")) new RootInstallOnlyChild(child);
|
|
699
748
|
},
|
|
700
749
|
);
|
|
701
750
|
|
|
@@ -717,7 +766,7 @@ export class DBXToolsTypeScriptProject
|
|
|
717
766
|
rootTsconfig?: DBXToolsRootTsconfig;
|
|
718
767
|
readonly extraWorkspaceMembers: readonly string[];
|
|
719
768
|
readonly releaseBranch: string;
|
|
720
|
-
|
|
769
|
+
readonly rootInstallOnly: boolean;
|
|
721
770
|
|
|
722
771
|
constructor(options: DBXToolsTypeScriptProjectOptions) {
|
|
723
772
|
validateReleaseOptions(options);
|
|
@@ -1331,7 +1380,6 @@ function initProject(
|
|
|
1331
1380
|
pythonRoot: options.releasePythonRoot,
|
|
1332
1381
|
validationTasks: options.releaseValidationTasks,
|
|
1333
1382
|
setupSteps: options.releaseSetupSteps,
|
|
1334
|
-
synthesisCommands: options.releaseSynthesisCommands,
|
|
1335
1383
|
});
|
|
1336
1384
|
}
|
|
1337
1385
|
}
|
|
@@ -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. */
|
|
@@ -247,10 +505,17 @@ export function resolvePythonNodeBindingModule(entrypoint: string, module: strin
|
|
|
247
505
|
}
|
|
248
506
|
|
|
249
507
|
function canonicalPath(path: string): string {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
508
|
+
const missing: string[] = [];
|
|
509
|
+
let candidate = resolve(path);
|
|
510
|
+
while (true) {
|
|
511
|
+
try {
|
|
512
|
+
return resolve(realpathSync(candidate), ...missing);
|
|
513
|
+
} catch {
|
|
514
|
+
const parent = dirname(candidate);
|
|
515
|
+
if (parent === candidate) return resolve(path);
|
|
516
|
+
missing.unshift(basename(candidate));
|
|
517
|
+
candidate = parent;
|
|
518
|
+
}
|
|
254
519
|
}
|
|
255
520
|
}
|
|
256
521
|
|
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"],
|
package/tasks/bump.ts
CHANGED
|
@@ -35,6 +35,11 @@ export async function main(): Promise<void> {
|
|
|
35
35
|
|
|
36
36
|
if (opts.synth) {
|
|
37
37
|
runTaskCommand(root, process.execPath, [".projenrc.ts"]);
|
|
38
|
+
// Package manifests are generated during the first synthesis, while
|
|
39
|
+
// cross-package derived artifacts can read those manifests earlier in
|
|
40
|
+
// the same pass. A second pass makes the new version visible to every
|
|
41
|
+
// generator and leaves release validation on a converged tree.
|
|
42
|
+
runTaskCommand(root, process.execPath, [".projenrc.ts"]);
|
|
38
43
|
}
|
|
39
44
|
logger.success(`workspace version synchronized at ${next.version}`);
|
|
40
45
|
});
|
|
@@ -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,21 @@ 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("--release-notes-instructions cannot be combined with supplied release notes");
|
|
262
|
+
}
|
|
263
|
+
if (
|
|
264
|
+
options.releaseNotes === false &&
|
|
265
|
+
(options.releaseNotesText !== undefined || options.releaseNotesFile !== undefined)
|
|
266
|
+
) {
|
|
267
|
+
throw new Error("--no-release-notes cannot be combined with supplied release notes");
|
|
268
|
+
}
|
|
250
269
|
|
|
251
270
|
if (install === "always") runTaskCommand(root, "bun", ["install"]);
|
|
252
271
|
if (selection.validation && hasPackageScript(root, "eslint:fix")) {
|
|
@@ -279,6 +298,8 @@ export async function runRelease(
|
|
|
279
298
|
prefix,
|
|
280
299
|
root,
|
|
281
300
|
version,
|
|
301
|
+
content: options.releaseNotesText,
|
|
302
|
+
file: options.releaseNotesFile,
|
|
282
303
|
instructions: options.releaseNotesInstructions,
|
|
283
304
|
});
|
|
284
305
|
}
|
|
@@ -380,9 +401,11 @@ export function createReleaseCommand(): Command {
|
|
|
380
401
|
.option("--no-docs", "skip documentation build and deployment")
|
|
381
402
|
.option(
|
|
382
403
|
"--no-validation",
|
|
383
|
-
"skip optional release validation tasks; version
|
|
404
|
+
"skip optional release validation tasks; workspace version checks remain mandatory",
|
|
384
405
|
)
|
|
385
406
|
.option("--no-release-notes", "skip writing docs/releases notes (Genie and git-log fallback)")
|
|
407
|
+
.option("--release-notes-text <markdown>", "write supplied markdown without invoking Genie")
|
|
408
|
+
.option("--release-notes-file <path>", "copy supplied markdown without invoking Genie")
|
|
386
409
|
.option(
|
|
387
410
|
"--release-notes-instructions <text>",
|
|
388
411
|
"append custom instructions to the Genie release-notes prompt",
|
|
@@ -410,6 +433,8 @@ export function createReleaseCommand(): Command {
|
|
|
410
433
|
profile?: string;
|
|
411
434
|
releaseNotes?: boolean;
|
|
412
435
|
releaseNotesInstructions?: string;
|
|
436
|
+
releaseNotesText?: string;
|
|
437
|
+
releaseNotesFile?: string;
|
|
413
438
|
},
|
|
414
439
|
) => {
|
|
415
440
|
await runRelease({
|
|
@@ -433,6 +458,8 @@ export function createReleaseCommand(): Command {
|
|
|
433
458
|
demoProfile: options.profile,
|
|
434
459
|
releaseNotes: options.releaseNotes,
|
|
435
460
|
releaseNotesInstructions: options.releaseNotesInstructions,
|
|
461
|
+
releaseNotesText: options.releaseNotesText,
|
|
462
|
+
releaseNotesFile: options.releaseNotesFile,
|
|
436
463
|
});
|
|
437
464
|
},
|
|
438
465
|
);
|