@nanobpm/urban 0.15.0 → 0.16.0
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.js +14 -3
- package/dist/toolkit/scaffold/workers.js +1 -1
- package/dist/worker.d.ts +3 -0
- package/dist/worker.js +18 -0
- package/package.json +7 -3
- package/src/cli.ts +12 -3
- package/src/toolkit/scaffold/workers.test.ts +1 -0
- package/src/toolkit/scaffold/workers.ts +1 -1
- package/src/worker.test.ts +104 -0
- package/src/worker.ts +21 -0
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,14 @@ function parse(argv) {
|
|
|
46
46
|
f.write = true;
|
|
47
47
|
else if (a === "--deno")
|
|
48
48
|
f.deno = true;
|
|
49
|
+
else if (a === "--code-first")
|
|
50
|
+
f.style = "code";
|
|
51
|
+
else if (a === "--style") {
|
|
52
|
+
const v = need(++i, a);
|
|
53
|
+
if (v !== "model" && v !== "code")
|
|
54
|
+
throw new Error(`flag --style requires "model" or "code" (got "${v}")`);
|
|
55
|
+
f.style = v;
|
|
56
|
+
}
|
|
49
57
|
else if (a === "--")
|
|
50
58
|
continue; // tolerate a bare "--" some runners inject (not an option terminator here)
|
|
51
59
|
else if (a === "--root")
|
|
@@ -70,7 +78,8 @@ function parse(argv) {
|
|
|
70
78
|
const USAGE = `urban — build and run Urban apps (nano.app.json)
|
|
71
79
|
|
|
72
80
|
Usage:
|
|
73
|
-
urban new <name> [--root <path>] [--deno] scaffold a new Urban app
|
|
81
|
+
urban new <name> [--root <path>] [--deno] [--style model|code] scaffold a new Urban app
|
|
82
|
+
(--code-first is shorthand for --style code)
|
|
74
83
|
urban check validate the manifest
|
|
75
84
|
urban gen [--check] derive artifacts (migrations, worker-io)
|
|
76
85
|
urban stubs [--write] scaffold write-once handler stubs from the model
|
|
@@ -183,14 +192,16 @@ async function cmdDeploy(f) {
|
|
|
183
192
|
async function cmdNew(f) {
|
|
184
193
|
const name = f._[1];
|
|
185
194
|
if (!name) {
|
|
186
|
-
console.error("usage: urban new <name> [--root <path>] [--deno]");
|
|
195
|
+
console.error("usage: urban new <name> [--root <path>] [--deno] [--style model|code] (or --code-first)");
|
|
187
196
|
return 1;
|
|
188
197
|
}
|
|
189
198
|
const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
|
|
190
|
-
const res = await scaffold({ name, dir, deno: f.deno });
|
|
199
|
+
const res = await scaffold({ name, dir, deno: f.deno, style: f.style });
|
|
191
200
|
console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
|
|
192
201
|
console.log(` cd ${res.dir}`);
|
|
193
202
|
console.log(` npm install && npm start`);
|
|
203
|
+
if (f.style === "code")
|
|
204
|
+
console.log(` npm run greet -- Adam # start a code-first instance`);
|
|
194
205
|
if (f.deno)
|
|
195
206
|
console.log(` # or on Deno: deno task start`);
|
|
196
207
|
return 0;
|
|
@@ -43,7 +43,7 @@ export function renderWorkerStub(taskType, typedIn, typedOut) {
|
|
|
43
43
|
return (`// Handler stub for the \`${taskType}\` service task, scaffolded from the model (ADR 0056).\n` +
|
|
44
44
|
`// This file is yours to edit — \`urban stubs\` will never overwrite it. Implement the body\n` +
|
|
45
45
|
`// below (use \`app.data.table(...)\` for state) and delete the throw.\n` +
|
|
46
|
-
`import type { AppJobHandler } from "@nanobpm/urban";\n` +
|
|
46
|
+
`import type { AppJobHandler } from "@nanobpm/urban/worker";\n` +
|
|
47
47
|
generatedImport +
|
|
48
48
|
`\n` +
|
|
49
49
|
`const handler: AppJobHandler${generic} = async (job, app) => {\n` +
|
package/dist/worker.d.ts
ADDED
package/dist/worker.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// `@nanobpm/urban/worker` — the runtime-free worker-authoring surface.
|
|
2
|
+
//
|
|
3
|
+
// A service-task handler only needs a handful of *types*: the handler signature and the
|
|
4
|
+
// shapes it is handed. Those all live under `runtime/core/`, which is guaranteed free of any
|
|
5
|
+
// `node:*` import or `Deno` reference (the ADR 0052 purity invariant, enforced by
|
|
6
|
+
// core/purity.test.ts). Importing them from the package barrel (`@nanobpm/urban`) instead
|
|
7
|
+
// drags in `runtime/index.ts`, which re-exports the Node and Deno host adapters — so the
|
|
8
|
+
// barrel's type graph transitively references `node:*` and a consumer must have `@types/node`
|
|
9
|
+
// resolvable (or `skipLibCheck` on) just to typecheck a handler.
|
|
10
|
+
//
|
|
11
|
+
// This entry re-exports ONLY those core authoring types, so a worker module (e.g. a scaffolded
|
|
12
|
+
// stub, ADR 0056) typechecks on Node or Deno, with or without `skipLibCheck`, and never needs
|
|
13
|
+
// `@types/node`. It is types-only: nothing here emits runtime code.
|
|
14
|
+
//
|
|
15
|
+
// (One caveat: `AppApi.sdk` exposes the full `@nanobpm/nano-sdk` client by design, so the
|
|
16
|
+
// handler surface also reaches the SDK's types. Those are kept node-free by the
|
|
17
|
+
// `@nanobpm/nano-sdk` >= 1.2.2 floor in package.json — see ADR 0056.)
|
|
18
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"types": "./dist/toolkit/index.d.ts",
|
|
44
44
|
"default": "./dist/toolkit/index.js"
|
|
45
45
|
},
|
|
46
|
+
"./worker": {
|
|
47
|
+
"types": "./dist/worker.d.ts",
|
|
48
|
+
"default": "./dist/worker.js"
|
|
49
|
+
},
|
|
46
50
|
"./source": "./src/index.ts"
|
|
47
51
|
},
|
|
48
52
|
"files": [
|
|
@@ -58,10 +62,10 @@
|
|
|
58
62
|
"test:deno": "deno test -A --no-check src/"
|
|
59
63
|
},
|
|
60
64
|
"dependencies": {
|
|
61
|
-
"@nanobpm/nano-sdk": "^1.2.
|
|
65
|
+
"@nanobpm/nano-sdk": "^1.2.2",
|
|
62
66
|
"@nanobpm/workflow": "^0.7.0",
|
|
63
67
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
64
|
-
"create-urban-app": "^0.
|
|
68
|
+
"create-urban-app": "^0.5.0"
|
|
65
69
|
},
|
|
66
70
|
"devDependencies": {
|
|
67
71
|
"@types/node": "^22.20.1",
|
package/src/cli.ts
CHANGED
|
@@ -44,6 +44,7 @@ interface Flags {
|
|
|
44
44
|
check: boolean;
|
|
45
45
|
write: boolean;
|
|
46
46
|
deno: boolean;
|
|
47
|
+
style?: "model" | "code";
|
|
47
48
|
help: boolean;
|
|
48
49
|
version: boolean;
|
|
49
50
|
_: string[];
|
|
@@ -63,6 +64,12 @@ function parse(argv: string[]): Flags {
|
|
|
63
64
|
else if (a === "--check") f.check = true;
|
|
64
65
|
else if (a === "--write") f.write = true;
|
|
65
66
|
else if (a === "--deno") f.deno = true;
|
|
67
|
+
else if (a === "--code-first") f.style = "code";
|
|
68
|
+
else if (a === "--style") {
|
|
69
|
+
const v = need(++i, a);
|
|
70
|
+
if (v !== "model" && v !== "code") throw new Error(`flag --style requires "model" or "code" (got "${v}")`);
|
|
71
|
+
f.style = v;
|
|
72
|
+
}
|
|
66
73
|
else if (a === "--") continue; // tolerate a bare "--" some runners inject (not an option terminator here)
|
|
67
74
|
else if (a === "--root") f.root = need(++i, a);
|
|
68
75
|
else if (a === "--manifest") f.manifest = need(++i, a);
|
|
@@ -82,7 +89,8 @@ function parse(argv: string[]): Flags {
|
|
|
82
89
|
const USAGE = `urban — build and run Urban apps (nano.app.json)
|
|
83
90
|
|
|
84
91
|
Usage:
|
|
85
|
-
urban new <name> [--root <path>] [--deno] scaffold a new Urban app
|
|
92
|
+
urban new <name> [--root <path>] [--deno] [--style model|code] scaffold a new Urban app
|
|
93
|
+
(--code-first is shorthand for --style code)
|
|
86
94
|
urban check validate the manifest
|
|
87
95
|
urban gen [--check] derive artifacts (migrations, worker-io)
|
|
88
96
|
urban stubs [--write] scaffold write-once handler stubs from the model
|
|
@@ -197,14 +205,15 @@ async function cmdDeploy(f: Flags): Promise<number> {
|
|
|
197
205
|
async function cmdNew(f: Flags): Promise<number> {
|
|
198
206
|
const name = f._[1];
|
|
199
207
|
if (!name) {
|
|
200
|
-
console.error("usage: urban new <name> [--root <path>] [--deno]");
|
|
208
|
+
console.error("usage: urban new <name> [--root <path>] [--deno] [--style model|code] (or --code-first)");
|
|
201
209
|
return 1;
|
|
202
210
|
}
|
|
203
211
|
const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
|
|
204
|
-
const res = await scaffold({ name, dir, deno: f.deno });
|
|
212
|
+
const res = await scaffold({ name, dir, deno: f.deno, style: f.style });
|
|
205
213
|
console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
|
|
206
214
|
console.log(` cd ${res.dir}`);
|
|
207
215
|
console.log(` npm install && npm start`);
|
|
216
|
+
if (f.style === "code") console.log(` npm run greet -- Adam # start a code-first instance`);
|
|
208
217
|
if (f.deno) console.log(` # or on Deno: deno task start`);
|
|
209
218
|
return 0;
|
|
210
219
|
}
|
|
@@ -40,6 +40,7 @@ test("slugifyTaskType makes a filesystem-safe slug", () => {
|
|
|
40
40
|
|
|
41
41
|
test("typed in+out stub imports both symbols and parameterises the handler", () => {
|
|
42
42
|
const stub = renderWorkerStub("pr.finalize", true, true);
|
|
43
|
+
assert.match(stub, /import type \{ AppJobHandler \} from "@nanobpm\/urban\/worker";/);
|
|
43
44
|
assert.match(stub, /import type \{ WorkerInputs, WorkerOutputs \} from "\.\.\/\.\.\/nano-generated\/worker-io\.d\.ts";/);
|
|
44
45
|
assert.match(stub, /AppJobHandler<WorkerInputs\["pr\.finalize"\], WorkerOutputs\["pr\.finalize"\]>/);
|
|
45
46
|
assert.match(stub, /export default handler;/);
|
|
@@ -94,7 +94,7 @@ export function renderWorkerStub(
|
|
|
94
94
|
`// Handler stub for the \`${taskType}\` service task, scaffolded from the model (ADR 0056).\n` +
|
|
95
95
|
`// This file is yours to edit — \`urban stubs\` will never overwrite it. Implement the body\n` +
|
|
96
96
|
`// below (use \`app.data.table(...)\` for state) and delete the throw.\n` +
|
|
97
|
-
`import type { AppJobHandler } from "@nanobpm/urban";\n` +
|
|
97
|
+
`import type { AppJobHandler } from "@nanobpm/urban/worker";\n` +
|
|
98
98
|
generatedImport +
|
|
99
99
|
`\n` +
|
|
100
100
|
`const handler: AppJobHandler${generic} = async (job, app) => {\n` +
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, join, normalize } from "node:path";
|
|
6
|
+
|
|
7
|
+
// Guard for the `@nanobpm/urban/worker` authoring surface. Its job is to keep urban's OWN
|
|
8
|
+
// host adapters (runtime/adapters/node.ts, runtime/adapters/deno.ts and their node:* imports)
|
|
9
|
+
// out of the type graph a worker handler imports — the coupling that otherwise forces
|
|
10
|
+
// `@types/node` / `skipLibCheck` on a consumer just to typecheck a handler.
|
|
11
|
+
//
|
|
12
|
+
// This walks the *relative*-import closure from worker.ts, so it covers urban's own reachable
|
|
13
|
+
// sources. It does NOT follow the bare `@nanobpm/nano-sdk` dependency behind `AppApi.sdk`
|
|
14
|
+
// (engine/sdk.ts) — that dependency's own node-freeness is enforced separately by the
|
|
15
|
+
// `@nanobpm/nano-sdk` >= 1.2.2 floor in package.json (which pulls @camunda8 >= 10.0.0-alpha.20,
|
|
16
|
+
// whose public types no longer reference node:worker_threads). Together, urban's node-free
|
|
17
|
+
// relative closure + that SDK floor make a scaffolded stub (ADR 0056) typecheck on Node or Deno
|
|
18
|
+
// with no `@types/node` and no `skipLibCheck`.
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
/** Collect relative-specifier imports/re-exports from a source file. */
|
|
23
|
+
function relativeSpecifiers(src: string): string[] {
|
|
24
|
+
const out: string[] = [];
|
|
25
|
+
const re = /\bfrom\s+["'](\.[^"']+)["']/g;
|
|
26
|
+
let m: RegExpExecArray | null;
|
|
27
|
+
while ((m = re.exec(src)) !== null) {
|
|
28
|
+
// Skip template-placeholder specifiers (e.g. `./${DOMAIN_DTS}` emitted as codegen strings),
|
|
29
|
+
// which are not real static import paths.
|
|
30
|
+
if (!m[1].includes("${")) out.push(m[1]);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve a relative specifier (with explicit .ts) against a containing file. */
|
|
36
|
+
function resolveSpec(fromFile: string, spec: string): string {
|
|
37
|
+
return normalize(join(dirname(fromFile), spec));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function closureFrom(entry: string): Promise<string[]> {
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
const stack = [entry];
|
|
43
|
+
while (stack.length > 0) {
|
|
44
|
+
const file = stack.pop()!;
|
|
45
|
+
if (seen.has(file)) continue;
|
|
46
|
+
seen.add(file);
|
|
47
|
+
let src: string;
|
|
48
|
+
try {
|
|
49
|
+
src = await readFile(file, "utf8");
|
|
50
|
+
} catch {
|
|
51
|
+
// A statically-extracted specifier that doesn't resolve to a real file (dynamic/generated
|
|
52
|
+
// path). Skip it — the purity assertions below only care about files that actually exist.
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const spec of relativeSpecifiers(src)) {
|
|
56
|
+
stack.push(resolveSpec(file, spec));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...seen];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test("@nanobpm/urban/worker closure is free of node:* and Deno", async () => {
|
|
63
|
+
const files = await closureFrom(join(here, "worker.ts"));
|
|
64
|
+
// Sanity: the walk actually reached the core authoring modules.
|
|
65
|
+
assert.ok(
|
|
66
|
+
files.some((f) => f.endsWith(join("core", "modules", "workers.ts"))),
|
|
67
|
+
"closure should include core/modules/workers.ts",
|
|
68
|
+
);
|
|
69
|
+
const offenders: string[] = [];
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const src = await readFile(file, "utf8");
|
|
72
|
+
if (/from\s+["']node:/.test(src)) offenders.push(`${file}: imports node:*`);
|
|
73
|
+
if (/\bDeno\s*\./.test(src)) offenders.push(`${file}: references Deno.*`);
|
|
74
|
+
}
|
|
75
|
+
assert.deepEqual(offenders, [], `worker surface purity violations:\n${offenders.join("\n")}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("@nanobpm/urban/worker excludes urban's host adapters (the barrel includes them)", async () => {
|
|
79
|
+
const workerClosure = await closureFrom(join(here, "worker.ts"));
|
|
80
|
+
const barrelClosure = await closureFrom(join(here, "index.ts"));
|
|
81
|
+
const adapters = [
|
|
82
|
+
join("runtime", "adapters", "node.ts"),
|
|
83
|
+
join("runtime", "adapters", "deno.ts"),
|
|
84
|
+
];
|
|
85
|
+
for (const a of adapters) {
|
|
86
|
+
assert.ok(
|
|
87
|
+
!workerClosure.some((f) => f.endsWith(a)),
|
|
88
|
+
`worker closure must NOT include ${a}`,
|
|
89
|
+
);
|
|
90
|
+
assert.ok(
|
|
91
|
+
barrelClosure.some((f) => f.endsWith(a)),
|
|
92
|
+
`barrel closure is expected to include ${a} (control)`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("@nanobpm/urban/worker re-exports the authoring types", async () => {
|
|
98
|
+
const src = await readFile(join(here, "worker.ts"), "utf8");
|
|
99
|
+
for (const sym of ["AppJobHandler", "EngineJob", "JobHandler", "AppApi"]) {
|
|
100
|
+
assert.match(src, new RegExp(`\\b${sym}\\b`), `worker.ts should export ${sym}`);
|
|
101
|
+
}
|
|
102
|
+
// Types-only surface: no value exports (nothing that would emit runtime code).
|
|
103
|
+
assert.doesNotMatch(src, /^export\s+(?!type\b)/m, "worker.ts must be a types-only re-export");
|
|
104
|
+
});
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// `@nanobpm/urban/worker` — the runtime-free worker-authoring surface.
|
|
2
|
+
//
|
|
3
|
+
// A service-task handler only needs a handful of *types*: the handler signature and the
|
|
4
|
+
// shapes it is handed. Those all live under `runtime/core/`, which is guaranteed free of any
|
|
5
|
+
// `node:*` import or `Deno` reference (the ADR 0052 purity invariant, enforced by
|
|
6
|
+
// core/purity.test.ts). Importing them from the package barrel (`@nanobpm/urban`) instead
|
|
7
|
+
// drags in `runtime/index.ts`, which re-exports the Node and Deno host adapters — so the
|
|
8
|
+
// barrel's type graph transitively references `node:*` and a consumer must have `@types/node`
|
|
9
|
+
// resolvable (or `skipLibCheck` on) just to typecheck a handler.
|
|
10
|
+
//
|
|
11
|
+
// This entry re-exports ONLY those core authoring types, so a worker module (e.g. a scaffolded
|
|
12
|
+
// stub, ADR 0056) typechecks on Node or Deno, with or without `skipLibCheck`, and never needs
|
|
13
|
+
// `@types/node`. It is types-only: nothing here emits runtime code.
|
|
14
|
+
//
|
|
15
|
+
// (One caveat: `AppApi.sdk` exposes the full `@nanobpm/nano-sdk` client by design, so the
|
|
16
|
+
// handler surface also reaches the SDK's types. Those are kept node-free by the
|
|
17
|
+
// `@nanobpm/nano-sdk` >= 1.2.2 floor in package.json — see ADR 0056.)
|
|
18
|
+
|
|
19
|
+
export type { AppJobHandler } from "./runtime/core/modules/workers.ts";
|
|
20
|
+
export type { EngineJob, JobHandler } from "./runtime/core/host.ts";
|
|
21
|
+
export type { AppApi } from "./runtime/core/context.ts";
|