@nanobpm/urban 0.36.0 → 0.37.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.
@@ -26,7 +26,7 @@ export interface WorkerStubPlan {
26
26
  stub: string;
27
27
  manifestEntry: StubManifestEntry;
28
28
  }
29
- export type SkipReason = "already-wired" | "orchestrator" | "duplicate";
29
+ export type SkipReason = "already-wired" | "orchestrator" | "duplicate" | "external";
30
30
  export interface SkippedWorker {
31
31
  taskType: string;
32
32
  reason: SkipReason;
@@ -41,8 +41,10 @@ export declare function slugifyTaskType(taskType: string): string;
41
41
  export declare function renderWorkerStub(taskType: string, typedIn: boolean, typedOut: boolean): string;
42
42
  /**
43
43
  * Plan write-once handler stubs from the models. A stub is planned for each service-task
44
- * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, and
45
- * not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
46
- * envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
44
+ * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, NOT
45
+ * declared external (serviced by an out-of-process worker), and not a duplicate of one already
46
+ * planned. `declaredTypeIds` are the manifest `types` ids — an envelope `in`/`out` is typed only
47
+ * when it names one (matching the worker-io deriver). `externalTaskTypes` are the manifest's
48
+ * declared external task types, which the app owns the model for but deliberately does not host.
47
49
  */
48
- export declare function planWorkerScaffold(models: ModelSource[], workers?: ScaffoldWorker[], declaredTypeIds?: Iterable<string>): WorkerScaffoldPlan;
50
+ export declare function planWorkerScaffold(models: ModelSource[], workers?: ScaffoldWorker[], declaredTypeIds?: Iterable<string>, externalTaskTypes?: Iterable<string>): WorkerScaffoldPlan;
@@ -57,15 +57,18 @@ export function renderWorkerStub(taskType, typedIn, typedOut) {
57
57
  }
58
58
  /**
59
59
  * Plan write-once handler stubs from the models. A stub is planned for each service-task
60
- * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, and
61
- * not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
62
- * envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
60
+ * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, NOT
61
+ * declared external (serviced by an out-of-process worker), and not a duplicate of one already
62
+ * planned. `declaredTypeIds` are the manifest `types` ids — an envelope `in`/`out` is typed only
63
+ * when it names one (matching the worker-io deriver). `externalTaskTypes` are the manifest's
64
+ * declared external task types, which the app owns the model for but deliberately does not host.
63
65
  */
64
- export function planWorkerScaffold(models, workers = [], declaredTypeIds = []) {
66
+ export function planWorkerScaffold(models, workers = [], declaredTypeIds = [], externalTaskTypes = []) {
65
67
  const wired = new Set(workers
66
68
  .map((w) => w?.taskType)
67
69
  .filter((t) => typeof t === "string" && t.length > 0));
68
70
  const declared = new Set(declaredTypeIds);
71
+ const external = new Set(externalTaskTypes);
69
72
  const plans = [];
70
73
  const skipped = [];
71
74
  const seenTaskTypes = new Set();
@@ -77,6 +80,10 @@ export function planWorkerScaffold(models, workers = [], declaredTypeIds = []) {
77
80
  skipped.push({ taskType, reason: "orchestrator" });
78
81
  continue;
79
82
  }
83
+ if (external.has(taskType)) {
84
+ skipped.push({ taskType, reason: "external" });
85
+ continue;
86
+ }
80
87
  if (wired.has(taskType)) {
81
88
  skipped.push({ taskType, reason: "already-wired" });
82
89
  continue;
@@ -15,7 +15,7 @@ export async function scaffoldWorkers(opts) {
15
15
  const manifest = JSON.parse(await io.readText(manifestPath));
16
16
  const models = await readModels(root, io, manifest);
17
17
  const declaredTypeIds = Object.keys(manifest.types ?? {});
18
- const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds);
18
+ const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds, manifest.externalTaskTypes ?? []);
19
19
  const outcomes = [];
20
20
  const wired = [];
21
21
  for (const plan of plans) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.36.0",
3
+ "version": "0.37.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",
@@ -62,7 +62,7 @@
62
62
  "test:deno": "deno test -A --no-check src/"
63
63
  },
64
64
  "dependencies": {
65
- "@nanobpm/nano-app-schema": "^0.4.0",
65
+ "@nanobpm/nano-app-schema": "^0.5.0",
66
66
  "@nanobpm/nano-sdk": "^1.2.3",
67
67
  "@nanobpm/workflow": "^0.8.0",
68
68
  "bpmn-auto-layout": "^2.0.0-alpha.2",
@@ -99,6 +99,16 @@ test("planWorkerScaffold: skips already-wired, orchestrator, and duplicates", ()
99
99
  assert.equal(reasons["t.new"], "duplicate");
100
100
  });
101
101
 
102
+ test("planWorkerScaffold: skips a task type declared external (out-of-process worker)", () => {
103
+ const models = [
104
+ model("a.bpmn", "p1", serviceTask("A", "senior:review"), serviceTask("B", "t.new")),
105
+ ];
106
+ const { plans, skipped } = planWorkerScaffold(models, [], [], ["senior:review"]);
107
+ assert.deepEqual(plans.map((p) => p.taskType), ["t.new"]);
108
+ const reasons = Object.fromEntries(skipped.map((s) => [s.taskType, s.reason]));
109
+ assert.equal(reasons["senior:review"], "external");
110
+ });
111
+
102
112
  test("planWorkerScaffold: colliding slugs get a numeric suffix", () => {
103
113
  const m = model("a.bpmn", "p", serviceTask("A", "orders.charge"), serviceTask("B", "orders-charge"));
104
114
  const { plans } = planWorkerScaffold([m], [], []);
@@ -195,3 +205,28 @@ test("scaffoldWorkers is idempotent: a second --write run is a no-op", async ()
195
205
  assert.equal(run2.manifestPatched, false, "no new wiring on the second run");
196
206
  assert.equal(JSON.stringify(io.files), snapshot, "files unchanged on re-run");
197
207
  });
208
+
209
+ test("scaffoldWorkers reads manifest.externalTaskTypes and skips those (never scaffolds/wires)", async () => {
210
+ const files = {
211
+ "/app/nano.app.json": JSON.stringify({
212
+ id: "demo",
213
+ models: { processes: ["processes/*.bpmn"] },
214
+ externalTaskTypes: ["senior:review"],
215
+ }),
216
+ "/app/processes/p.bpmn": model("p.bpmn", "p",
217
+ serviceTask("A", "senior:review"),
218
+ serviceTask("B", "t.new"),
219
+ ).xml,
220
+ };
221
+ const io = memIO(files);
222
+ const run = await scaffoldWorkers({ root: "/app", io, write: true });
223
+ // Only the app-hosted task is scaffolded; the external one is skipped.
224
+ assert.deepEqual(run.outcomes.map((o) => o.taskType), ["t.new"]);
225
+ assert.ok(run.skipped.some((s) => s.taskType === "senior:review" && s.reason === "external"));
226
+ assert.equal(io.files["/app/workers/senior-review/worker.ts"], undefined, "external task gets no stub");
227
+ const manifest = JSON.parse(io.files["/app/nano.app.json"]);
228
+ assert.ok(
229
+ !manifest.workers?.some((w: { taskType: string }) => w.taskType === "senior:review"),
230
+ "external task is never wired into workers[]",
231
+ );
232
+ });
@@ -39,7 +39,7 @@ export interface WorkerStubPlan {
39
39
  manifestEntry: StubManifestEntry;
40
40
  }
41
41
 
42
- export type SkipReason = "already-wired" | "orchestrator" | "duplicate";
42
+ export type SkipReason = "already-wired" | "orchestrator" | "duplicate" | "external";
43
43
 
44
44
  export interface SkippedWorker {
45
45
  taskType: string;
@@ -110,14 +110,17 @@ export function renderWorkerStub(
110
110
 
111
111
  /**
112
112
  * Plan write-once handler stubs from the models. A stub is planned for each service-task
113
- * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, and
114
- * not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
115
- * envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
113
+ * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, NOT
114
+ * declared external (serviced by an out-of-process worker), and not a duplicate of one already
115
+ * planned. `declaredTypeIds` are the manifest `types` ids — an envelope `in`/`out` is typed only
116
+ * when it names one (matching the worker-io deriver). `externalTaskTypes` are the manifest's
117
+ * declared external task types, which the app owns the model for but deliberately does not host.
116
118
  */
117
119
  export function planWorkerScaffold(
118
120
  models: ModelSource[],
119
121
  workers: ScaffoldWorker[] = [],
120
122
  declaredTypeIds: Iterable<string> = [],
123
+ externalTaskTypes: Iterable<string> = [],
121
124
  ): WorkerScaffoldPlan {
122
125
  const wired = new Set(
123
126
  workers
@@ -125,6 +128,7 @@ export function planWorkerScaffold(
125
128
  .filter((t): t is string => typeof t === "string" && t.length > 0),
126
129
  );
127
130
  const declared = new Set(declaredTypeIds);
131
+ const external = new Set(externalTaskTypes);
128
132
 
129
133
  const plans: WorkerStubPlan[] = [];
130
134
  const skipped: SkippedWorker[] = [];
@@ -138,6 +142,10 @@ export function planWorkerScaffold(
138
142
  skipped.push({ taskType, reason: "orchestrator" });
139
143
  continue;
140
144
  }
145
+ if (external.has(taskType)) {
146
+ skipped.push({ taskType, reason: "external" });
147
+ continue;
148
+ }
141
149
  if (wired.has(taskType)) {
142
150
  skipped.push({ taskType, reason: "already-wired" });
143
151
  continue;
@@ -19,6 +19,7 @@ interface ScaffoldManifest {
19
19
  types?: Record<string, unknown>;
20
20
  models?: { processes?: string[] };
21
21
  workers?: ScaffoldWorker[];
22
+ externalTaskTypes?: string[];
22
23
  api?: { spec?: string; dir?: string };
23
24
  }
24
25
 
@@ -64,7 +65,12 @@ export async function scaffoldWorkers(opts: ScaffoldOptions): Promise<ScaffoldRu
64
65
  const models = await readModels(root, io, manifest);
65
66
  const declaredTypeIds = Object.keys(manifest.types ?? {});
66
67
 
67
- const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds);
68
+ const { plans, skipped } = planWorkerScaffold(
69
+ models,
70
+ manifest.workers ?? [],
71
+ declaredTypeIds,
72
+ manifest.externalTaskTypes ?? [],
73
+ );
68
74
 
69
75
  const outcomes: StubOutcome[] = [];
70
76
  const wired: StubManifestEntry[] = [];