@nanobpm/urban 0.96.0 → 0.96.1
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/dist/cli.d.ts +8 -0
- package/dist/cli.js +24 -12
- package/package.json +1 -1
- package/src/bin.mjs +13 -6
- package/src/bin.test.ts +74 -0
- package/src/cli.ts +25 -12
package/dist/cli.d.ts
CHANGED
|
@@ -23,4 +23,12 @@ interface Flags {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function cmdData(f: Flags, readInput?: () => Promise<string>): Promise<number>;
|
|
25
25
|
export declare function main(argv: string[]): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* Run the CLI as the process entrypoint: `main(argv)`, then drain stdio and exit with its code.
|
|
28
|
+
* A negative code means a long-running command (`run`, `dev`) owns the process, so it is left
|
|
29
|
+
* alive. Exported so the `urban` bin (`bin.mjs`) can run the CLI **in-process** via `import()`
|
|
30
|
+
* instead of spawning a second Node (nano-ide#589): every `urban` command, notably each
|
|
31
|
+
* gateway `urban data` op, used to pay two Node start-ups.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runCli(argv: string[]): Promise<void>;
|
|
26
34
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -457,24 +457,23 @@ async function flushStdio() {
|
|
|
457
457
|
return;
|
|
458
458
|
await Promise.all([flushStream(p.stdout), flushStream(p.stderr)]);
|
|
459
459
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
460
|
+
/**
|
|
461
|
+
* Run the CLI as the process entrypoint: `main(argv)`, then drain stdio and exit with its code.
|
|
462
|
+
* A negative code means a long-running command (`run`, `dev`) owns the process, so it is left
|
|
463
|
+
* alive. Exported so the `urban` bin (`bin.mjs`) can run the CLI **in-process** via `import()`
|
|
464
|
+
* instead of spawning a second Node (nano-ide#589): every `urban` command, notably each
|
|
465
|
+
* gateway `urban data` op, used to pay two Node start-ups.
|
|
466
|
+
*/
|
|
467
|
+
export function runCli(argv) {
|
|
468
|
+
const proc = processGlobal();
|
|
469
|
+
const deno = denoGlobal();
|
|
471
470
|
const exit = (code) => {
|
|
472
471
|
if (proc?.exit)
|
|
473
472
|
proc.exit(code);
|
|
474
473
|
else if (deno?.exit)
|
|
475
474
|
deno.exit(code);
|
|
476
475
|
};
|
|
477
|
-
main(argv).then(async (code) => {
|
|
476
|
+
return main(argv).then(async (code) => {
|
|
478
477
|
if (code >= 0) {
|
|
479
478
|
await flushStdio();
|
|
480
479
|
exit(code);
|
|
@@ -485,3 +484,16 @@ if (importMetaMain === true || nodeMain) {
|
|
|
485
484
|
exit(1);
|
|
486
485
|
});
|
|
487
486
|
}
|
|
487
|
+
const proc = processGlobal();
|
|
488
|
+
const deno = denoGlobal();
|
|
489
|
+
const importMetaMain = Reflect.get(import.meta, "main");
|
|
490
|
+
const argv1 = proc?.argv?.[1];
|
|
491
|
+
const nodeMain = argv1 ? import.meta.url === pathToFileURL(argv1).href : false;
|
|
492
|
+
if (importMetaMain === true || nodeMain) {
|
|
493
|
+
// Node passes args via process.argv (slice off exec+script); Deno (run directly)
|
|
494
|
+
// exposes them on Deno.args. Prefer whichever actually carries args.
|
|
495
|
+
const fromNode = proc?.argv?.slice(2);
|
|
496
|
+
const fromDeno = deno?.args;
|
|
497
|
+
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
498
|
+
void runCli(argv);
|
|
499
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.96.
|
|
3
|
+
"version": "0.96.1",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/bin.mjs
CHANGED
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
// published as compiled JS + .d.ts because Node cannot strip types for files under
|
|
4
4
|
// node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so no type-stripping flag
|
|
5
5
|
// is needed here.
|
|
6
|
+
//
|
|
7
|
+
// The CLI is loaded IN-PROCESS (dynamic import + runCli) rather than spawned as a second
|
|
8
|
+
// Node: a spawn doubled the start-up cost of every command, and the gateway runs `urban
|
|
9
|
+
// data` once per data op (nano-ide#589, nano-bpm#1340). dist/cli.js only auto-runs when it
|
|
10
|
+
// is argv[1] itself, so importing it here does not start it twice.
|
|
6
11
|
|
|
7
|
-
import {
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
13
|
import { dirname, join } from "node:path";
|
|
10
14
|
|
|
11
15
|
const cli = join(dirname(fileURLToPath(import.meta.url)), "..", "dist", "cli.js");
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
let runCli;
|
|
18
|
+
try {
|
|
19
|
+
({ runCli } = await import(pathToFileURL(cli).href));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(String(err?.message ?? err));
|
|
16
22
|
process.exit(1);
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
// runCli owns the exit code, stdio flushing and long-running commands (run/dev).
|
|
25
|
+
await runCli(process.argv.slice(2));
|
package/src/bin.test.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The `urban` bin (`bin.mjs`) must run the compiled CLI in the SAME Node process (nano-ide#589):
|
|
2
|
+
// the gateway spawns `urban data` per data op, and the old spawnSync bootstrap paid two Node
|
|
3
|
+
// start-ups for every one. Exercises the real bin against dist/ (CI builds before testing).
|
|
4
|
+
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
+
|
|
13
|
+
const bin = fileURLToPath(new URL("./bin.mjs", import.meta.url));
|
|
14
|
+
const dist = fileURLToPath(new URL("../dist/cli.js", import.meta.url));
|
|
15
|
+
|
|
16
|
+
function runBin(
|
|
17
|
+
args: string[],
|
|
18
|
+
stdin: string,
|
|
19
|
+
env: NodeJS.ProcessEnv,
|
|
20
|
+
): Promise<{ stdout: string; code: number | null }> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(process.execPath, [bin, ...args], {
|
|
23
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
24
|
+
env: { ...process.env, ...env },
|
|
25
|
+
});
|
|
26
|
+
const chunks: Buffer[] = [];
|
|
27
|
+
child.stdout.on("data", (c: Buffer) => chunks.push(c));
|
|
28
|
+
child.on("error", reject);
|
|
29
|
+
child.on("close", (code) => resolve({ stdout: Buffer.concat(chunks).toString("utf8"), code }));
|
|
30
|
+
child.stdin.end(stdin);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `bin.mjs` is the npm bin, so it always runs under Node: skip under Deno (`test:deno`), whose
|
|
35
|
+
// process.execPath is not Node (same as generated-lint.test.ts).
|
|
36
|
+
const runtimeIsNode = !("Deno" in globalThis);
|
|
37
|
+
|
|
38
|
+
test("bin.mjs runs the CLI in-process: one Node per command, reply + exit code intact", {
|
|
39
|
+
skip: runtimeIsNode ? false : "Node-only: spawns bin.mjs via process.execPath (Deno's execPath is not Node)",
|
|
40
|
+
}, async (t) => {
|
|
41
|
+
if (!existsSync(dist)) {
|
|
42
|
+
t.skip("dist/cli.js not built (run `npm run build`); CI builds before testing");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const dir = mkdtempSync(join(tmpdir(), "urban-bin-"));
|
|
46
|
+
try {
|
|
47
|
+
// A preload every Node process runs (NODE_OPTIONS is inherited by child Nodes), recording
|
|
48
|
+
// its pid. The old spawnSync bootstrap would record two.
|
|
49
|
+
const pids = join(dir, "pids.txt");
|
|
50
|
+
const preload = join(dir, "preload.mjs");
|
|
51
|
+
writeFileSync(
|
|
52
|
+
preload,
|
|
53
|
+
`import { appendFileSync } from "node:fs";\nappendFileSync(${JSON.stringify(pids)}, process.pid + "\\n");\n`,
|
|
54
|
+
);
|
|
55
|
+
const env = { NODE_OPTIONS: `--import=${pathToFileURL(preload).href}` };
|
|
56
|
+
|
|
57
|
+
// An unreadable request: `urban data` still answers with a parseable envelope and exits 0.
|
|
58
|
+
const { stdout, code } = await runBin(["data", "--root", dir], "not json", env);
|
|
59
|
+
assert.equal(code, 0, `bin exited ${code}, expected 0`);
|
|
60
|
+
const reply: unknown = JSON.parse(stdout);
|
|
61
|
+
assert.ok(reply !== null && typeof reply === "object", "reply is not an object");
|
|
62
|
+
assert.equal(Reflect.get(reply, "ok"), false);
|
|
63
|
+
assert.match(String(Reflect.get(reply, "error")), /^bad request:/);
|
|
64
|
+
|
|
65
|
+
const started = readFileSync(pids, "utf8").trim().split("\n").filter(Boolean);
|
|
66
|
+
assert.equal(started.length, 1, `expected 1 Node process, got ${started.length}`);
|
|
67
|
+
|
|
68
|
+
// A failing command's non-zero exit code still propagates through the bin.
|
|
69
|
+
const unknown = await runBin(["no-such-command"], "", {});
|
|
70
|
+
assert.equal(unknown.code, 1);
|
|
71
|
+
} finally {
|
|
72
|
+
rmSync(dir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -472,22 +472,21 @@ async function flushStdio(): Promise<void> {
|
|
|
472
472
|
await Promise.all([flushStream(p.stdout), flushStream(p.stderr)]);
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
475
|
+
/**
|
|
476
|
+
* Run the CLI as the process entrypoint: `main(argv)`, then drain stdio and exit with its code.
|
|
477
|
+
* A negative code means a long-running command (`run`, `dev`) owns the process, so it is left
|
|
478
|
+
* alive. Exported so the `urban` bin (`bin.mjs`) can run the CLI **in-process** via `import()`
|
|
479
|
+
* instead of spawning a second Node (nano-ide#589): every `urban` command, notably each
|
|
480
|
+
* gateway `urban data` op, used to pay two Node start-ups.
|
|
481
|
+
*/
|
|
482
|
+
export function runCli(argv: string[]): Promise<void> {
|
|
483
|
+
const proc = processGlobal();
|
|
484
|
+
const deno = denoGlobal();
|
|
486
485
|
const exit = (code: number) => {
|
|
487
486
|
if (proc?.exit) proc.exit(code);
|
|
488
487
|
else if (deno?.exit) deno.exit(code);
|
|
489
488
|
};
|
|
490
|
-
main(argv).then(
|
|
489
|
+
return main(argv).then(
|
|
491
490
|
async (code) => {
|
|
492
491
|
if (code >= 0) {
|
|
493
492
|
await flushStdio();
|
|
@@ -501,3 +500,17 @@ if (importMetaMain === true || nodeMain) {
|
|
|
501
500
|
},
|
|
502
501
|
);
|
|
503
502
|
}
|
|
503
|
+
|
|
504
|
+
const proc = processGlobal();
|
|
505
|
+
const deno = denoGlobal();
|
|
506
|
+
const importMetaMain: unknown = Reflect.get(import.meta, "main");
|
|
507
|
+
const argv1 = proc?.argv?.[1];
|
|
508
|
+
const nodeMain = argv1 ? import.meta.url === pathToFileURL(argv1).href : false;
|
|
509
|
+
if (importMetaMain === true || nodeMain) {
|
|
510
|
+
// Node passes args via process.argv (slice off exec+script); Deno (run directly)
|
|
511
|
+
// exposes them on Deno.args. Prefer whichever actually carries args.
|
|
512
|
+
const fromNode = proc?.argv?.slice(2);
|
|
513
|
+
const fromDeno = deno?.args;
|
|
514
|
+
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
515
|
+
void runCli(argv);
|
|
516
|
+
}
|