@dbx-tools/projen 0.9.93 → 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 +42 -6
- package/index.ts +3 -1
- package/package.json +4 -4
- package/src/dev-watch.ts +15 -0
- package/src/pnpm-workspace.ts +1 -0
- package/src/project-js.ts +67 -14
- package/src/python-node-bindings.ts +279 -21
- package/src/release.ts +0 -10
- package/src/tags.ts +2 -6
- package/src/tsconfig.ts +1 -1
- package/tasks/dev-watch.ts +339 -0
- package/tasks/python-node-bindings.ts +64 -100
- package/tasks/release-notes.ts +24 -1
- package/tasks/release.ts +45 -4
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Restart a development command after relevant workspace changes settle. */
|
|
3
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { configUtils } from "@dbx-tools/core";
|
|
5
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
6
|
+
import { watch as pathWatch } from "@dbx-tools/path";
|
|
7
|
+
import { log } from "@dbx-tools/shared-core";
|
|
8
|
+
import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
|
|
9
|
+
import {
|
|
10
|
+
DEV_RESTART_DEBOUNCE_MS,
|
|
11
|
+
DEV_RESTART_KEY,
|
|
12
|
+
DEV_WATCH_TASK,
|
|
13
|
+
SERVER_WATCH_DISABLED_ENV,
|
|
14
|
+
} from "../src/dev-watch.ts";
|
|
15
|
+
import {
|
|
16
|
+
readPackageManifest,
|
|
17
|
+
recordedPackages,
|
|
18
|
+
resolveRepoRoot,
|
|
19
|
+
workspaceDependencyDirectories,
|
|
20
|
+
} from "../src/packages.ts";
|
|
21
|
+
|
|
22
|
+
const logger = log.logger("projen:dev-watch");
|
|
23
|
+
const SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
24
|
+
|
|
25
|
+
export interface DevWatchOptions {
|
|
26
|
+
readonly command: readonly string[];
|
|
27
|
+
readonly debounceMs: number;
|
|
28
|
+
readonly restartKey: string;
|
|
29
|
+
/** When true, run the command once and skip file-change restarts. */
|
|
30
|
+
readonly serverWatchDisabled: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WorkspacePackage {
|
|
34
|
+
readonly dir: string;
|
|
35
|
+
readonly name?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function debounceMilliseconds(value: string): number {
|
|
39
|
+
const parsed = configUtils.toNumber(value);
|
|
40
|
+
if (parsed === undefined || parsed < 0) {
|
|
41
|
+
throw new InvalidArgumentError("must be a non-negative number of milliseconds");
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function singleKey(value: string): string {
|
|
47
|
+
if (Array.from(value).length !== 1) {
|
|
48
|
+
throw new InvalidArgumentError("must be exactly one character");
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Parse watcher flags while passing every token after the command through unchanged. */
|
|
54
|
+
export function parseDevWatchOptions(args: string[]): DevWatchOptions {
|
|
55
|
+
const program = new Command()
|
|
56
|
+
.name(DEV_WATCH_TASK)
|
|
57
|
+
.description("Restart a development command after watched changes settle")
|
|
58
|
+
.exitOverride()
|
|
59
|
+
.configureOutput({
|
|
60
|
+
writeErr: (message) => {
|
|
61
|
+
if (import.meta.main) process.stderr.write(message);
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
.enablePositionalOptions()
|
|
65
|
+
.passThroughOptions()
|
|
66
|
+
.addOption(
|
|
67
|
+
new Option("--debounce-ms <milliseconds>", "quiet period before restart")
|
|
68
|
+
.default(DEV_RESTART_DEBOUNCE_MS)
|
|
69
|
+
.argParser(debounceMilliseconds),
|
|
70
|
+
)
|
|
71
|
+
.addOption(
|
|
72
|
+
new Option("--restart-key <key>", "interactive immediate-restart key")
|
|
73
|
+
.default(DEV_RESTART_KEY)
|
|
74
|
+
.argParser(singleKey),
|
|
75
|
+
)
|
|
76
|
+
.addOption(
|
|
77
|
+
new Option(
|
|
78
|
+
"--server-watch-disabled",
|
|
79
|
+
`run the command once without watching (${SERVER_WATCH_DISABLED_ENV})`,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
.argument("<command>")
|
|
83
|
+
.argument("[args...]")
|
|
84
|
+
.parse(["bun", DEV_WATCH_TASK, ...args]);
|
|
85
|
+
const options = program.opts<{
|
|
86
|
+
debounceMs: number;
|
|
87
|
+
restartKey: string;
|
|
88
|
+
serverWatchDisabled?: boolean;
|
|
89
|
+
}>();
|
|
90
|
+
const [command, commandArgs = []] = program.processedArgs as [string, string[]?];
|
|
91
|
+
const serverWatchDisabled =
|
|
92
|
+
options.serverWatchDisabled === true ||
|
|
93
|
+
configUtils.boolean(undefined, SERVER_WATCH_DISABLED_ENV, configUtils.ENV_ONLY) === true;
|
|
94
|
+
if (serverWatchDisabled) process.env[SERVER_WATCH_DISABLED_ENV] = "1";
|
|
95
|
+
return {
|
|
96
|
+
command: [command, ...commandArgs],
|
|
97
|
+
debounceMs: options.debounceMs,
|
|
98
|
+
restartKey: options.restartKey,
|
|
99
|
+
serverWatchDisabled,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function containsPath(parent: string, candidate: string): boolean {
|
|
104
|
+
const rel = relative(parent, candidate);
|
|
105
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function workspacePackages(root: string): WorkspacePackage[] {
|
|
109
|
+
return recordedPackages(root)
|
|
110
|
+
.map(({ dir }) => {
|
|
111
|
+
const name = readPackageManifest(dir)?.name;
|
|
112
|
+
return { dir, name: typeof name === "string" ? name : undefined };
|
|
113
|
+
})
|
|
114
|
+
.sort((left, right) => right.dir.length - left.dir.length);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function optionArgument(
|
|
118
|
+
command: readonly string[],
|
|
119
|
+
longName: string,
|
|
120
|
+
shortName?: string,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
for (let index = 0; index < command.length; index++) {
|
|
123
|
+
const token = command[index]!;
|
|
124
|
+
if (token === longName || (shortName !== undefined && token === shortName)) {
|
|
125
|
+
return command[index + 1];
|
|
126
|
+
}
|
|
127
|
+
if (token.startsWith(`${longName}=`)) return token.slice(longName.length + 1);
|
|
128
|
+
if (shortName !== undefined && token.startsWith(`${shortName}=`)) {
|
|
129
|
+
return token.slice(shortName.length + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function commandWorkingDirectory(command: readonly string[], cwd: string): string {
|
|
136
|
+
const configured = optionArgument(command, "--cwd");
|
|
137
|
+
return configured ? resolve(cwd, configured) : cwd;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function commandPackage(
|
|
141
|
+
command: readonly string[],
|
|
142
|
+
cwd: string,
|
|
143
|
+
packages: readonly WorkspacePackage[],
|
|
144
|
+
): WorkspacePackage | undefined {
|
|
145
|
+
const selected = optionArgument(command, "--filter", "-F");
|
|
146
|
+
if (selected) {
|
|
147
|
+
const filtered = packages.find(({ name }) => name === selected);
|
|
148
|
+
if (filtered) return filtered;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const commandCwd = commandWorkingDirectory(command, cwd);
|
|
152
|
+
for (const token of command) {
|
|
153
|
+
if (token.startsWith("-")) continue;
|
|
154
|
+
const candidate = resolve(commandCwd, token);
|
|
155
|
+
const owner = packages.find(({ dir }) => containsPath(dir, candidate));
|
|
156
|
+
if (owner) return owner;
|
|
157
|
+
}
|
|
158
|
+
return packages.find(({ dir }) => containsPath(dir, commandCwd));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Source directories most likely to affect the command, using workspace dependency metadata. */
|
|
162
|
+
export function devWatchDirectories(
|
|
163
|
+
command: readonly string[],
|
|
164
|
+
cwd: string = process.cwd(),
|
|
165
|
+
): string[] {
|
|
166
|
+
const root = resolveRepoRoot(cwd);
|
|
167
|
+
const packages = workspacePackages(root);
|
|
168
|
+
const owner = commandPackage(command, cwd, packages);
|
|
169
|
+
if (!owner) return [commandWorkingDirectory(command, cwd)];
|
|
170
|
+
const dependencies = owner.name ? workspaceDependencyDirectories(owner.name, root) : [];
|
|
171
|
+
return dependencies.length > 0 ? dependencies : [owner.dir];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Compare one raw terminal character with the configured restart key. */
|
|
175
|
+
export function isRestartKey(key: string, restartKey: string): boolean {
|
|
176
|
+
return key === restartKey || key.toLocaleLowerCase() === restartKey.toLocaleLowerCase();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Supervise one command until the user or host stops the task. */
|
|
180
|
+
export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
|
|
181
|
+
let options: DevWatchOptions;
|
|
182
|
+
try {
|
|
183
|
+
options = parseDevWatchOptions(args);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (!(err instanceof CommanderError)) throw err;
|
|
186
|
+
if (err.exitCode !== 0) process.exitCode = err.exitCode;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const cwd = process.cwd();
|
|
190
|
+
if (options.serverWatchDisabled) {
|
|
191
|
+
logger.info(`${SERVER_WATCH_DISABLED_ENV} is set; running without file watching`);
|
|
192
|
+
const [command, ...commandArgs] = options.command;
|
|
193
|
+
const { exitCode } = await exec.spawn(command!, commandArgs, {
|
|
194
|
+
cwd,
|
|
195
|
+
stdin: "ignore",
|
|
196
|
+
stdout: "inherit",
|
|
197
|
+
stderr: "inherit",
|
|
198
|
+
check: false,
|
|
199
|
+
});
|
|
200
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const root = resolveRepoRoot(cwd);
|
|
205
|
+
const watchDirectories = devWatchDirectories(options.command, cwd);
|
|
206
|
+
const commandLabel = options.command.map((part) => JSON.stringify(part)).join(" ");
|
|
207
|
+
let child: exec.ChildProcessResult | undefined;
|
|
208
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
209
|
+
let restartReason: string | undefined;
|
|
210
|
+
let restarting = false;
|
|
211
|
+
let stopping = false;
|
|
212
|
+
|
|
213
|
+
const watcher = pathWatch.watchFiles(watchDirectories, {
|
|
214
|
+
cwd: root,
|
|
215
|
+
ignoreInitial: true,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
async function stopChild(): Promise<void> {
|
|
219
|
+
const current = child;
|
|
220
|
+
if (!current) return;
|
|
221
|
+
child = undefined;
|
|
222
|
+
try {
|
|
223
|
+
await exec.kill(current, {
|
|
224
|
+
gracefulTimeoutMs: SHUTDOWN_TIMEOUT_MS,
|
|
225
|
+
forceTimeoutMs: SHUTDOWN_TIMEOUT_MS,
|
|
226
|
+
});
|
|
227
|
+
} catch (err) {
|
|
228
|
+
logger.error("failed to stop command process tree:", err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function startChild(reason: string): void {
|
|
233
|
+
logger.info(`${reason}; starting ${commandLabel}`);
|
|
234
|
+
const [command, ...commandArgs] = options.command;
|
|
235
|
+
const current = exec.spawn(command!, commandArgs, {
|
|
236
|
+
cwd,
|
|
237
|
+
stdin: "ignore",
|
|
238
|
+
stdout: "inherit",
|
|
239
|
+
stderr: "inherit",
|
|
240
|
+
check: false,
|
|
241
|
+
});
|
|
242
|
+
child = current;
|
|
243
|
+
void current
|
|
244
|
+
.then(({ exitCode }) => {
|
|
245
|
+
if (child !== current || stopping) return;
|
|
246
|
+
child = undefined;
|
|
247
|
+
logger.warn(
|
|
248
|
+
`command exited with code ${exitCode}; press ${JSON.stringify(options.restartKey)} or change a watched file to restart`,
|
|
249
|
+
);
|
|
250
|
+
})
|
|
251
|
+
.catch((err) => {
|
|
252
|
+
if (child !== current || stopping) return;
|
|
253
|
+
child = undefined;
|
|
254
|
+
logger.error("command failed:", err);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function drainRestarts(): Promise<void> {
|
|
259
|
+
if (restarting) return;
|
|
260
|
+
restarting = true;
|
|
261
|
+
try {
|
|
262
|
+
while (restartReason !== undefined && !stopping) {
|
|
263
|
+
const reason = restartReason;
|
|
264
|
+
restartReason = undefined;
|
|
265
|
+
await stopChild();
|
|
266
|
+
if (!stopping) startChild(reason);
|
|
267
|
+
}
|
|
268
|
+
} finally {
|
|
269
|
+
restarting = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function requestRestart(reason: string): void {
|
|
274
|
+
restartReason = reason;
|
|
275
|
+
void drainRestarts();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function scheduleRestart(): void {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
timer = setTimeout(
|
|
281
|
+
() => requestRestart(`changes settled for ${options.debounceMs}ms`),
|
|
282
|
+
options.debounceMs,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
watcher.on("all", scheduleRestart);
|
|
287
|
+
watcher.on("error", (err) => logger.error("watcher error:", err));
|
|
288
|
+
watcher.on("ready", () => {
|
|
289
|
+
const packages = watchDirectories.length;
|
|
290
|
+
logger.info(
|
|
291
|
+
`watching ${packages} workspace ${packages === 1 ? "directory" : "directories"} with ${options.debounceMs}ms restart debounce`,
|
|
292
|
+
);
|
|
293
|
+
logger.info(`press ${JSON.stringify(options.restartKey)} to restart; Ctrl-C to stop`);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const stdin = process.stdin;
|
|
297
|
+
const wasRaw = stdin.isTTY ? Boolean(stdin.isRaw) : false;
|
|
298
|
+
const onInput = (chunk: Buffer | string): void => {
|
|
299
|
+
for (const key of String(chunk)) {
|
|
300
|
+
if (key === "\u0003") {
|
|
301
|
+
void shutdown();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (isRestartKey(key, options.restartKey)) {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
requestRestart("manual restart requested");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
311
|
+
stdin.resume();
|
|
312
|
+
stdin.on("data", onInput);
|
|
313
|
+
|
|
314
|
+
let finish!: () => void;
|
|
315
|
+
const complete = new Promise<void>((resolveComplete) => {
|
|
316
|
+
finish = resolveComplete;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
async function shutdown(): Promise<void> {
|
|
320
|
+
if (stopping) return;
|
|
321
|
+
stopping = true;
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
restartReason = undefined;
|
|
324
|
+
stdin.off("data", onInput);
|
|
325
|
+
if (stdin.isTTY) stdin.setRawMode(wasRaw);
|
|
326
|
+
stdin.pause();
|
|
327
|
+
await Promise.allSettled([watcher.close(), stopChild()]);
|
|
328
|
+
finish();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
|
|
332
|
+
process.once(signal, () => void shutdown());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
requestRestart("initial start");
|
|
336
|
+
await complete;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (import.meta.main) await main();
|
|
@@ -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",
|