@nanobpm/urban 0.96.2 → 0.96.3
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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/toolkit/gen.d.ts +10 -0
- package/dist/toolkit/gen.js +1 -1
- package/dist/toolkit/generate.js +9 -1
- package/dist/toolkit/models.d.ts +7 -0
- package/dist/toolkit/models.js +4 -1
- package/dist/toolkit/scaffold/workers.d.ts +9 -6
- package/dist/toolkit/scaffold/workers.js +13 -5
- package/dist/toolkit/scaffold.d.ts +17 -0
- package/dist/toolkit/scaffold.js +9 -2
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/toolkit/gen.ts +11 -1
- package/src/toolkit/generate.test.ts +86 -1
- package/src/toolkit/generate.ts +9 -1
- package/src/toolkit/models.test.ts +21 -0
- package/src/toolkit/models.ts +10 -1
- package/src/toolkit/scaffold/workers.test.ts +65 -0
- package/src/toolkit/scaffold/workers.ts +14 -5
- package/src/toolkit/scaffold.ts +27 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./runtime/index.ts";
|
|
2
2
|
export { createCamundaClient } from "@nanobpm/nano-sdk";
|
|
3
3
|
export * from "./toolkit/index.ts";
|
|
4
|
-
export { defineFlow, declarativeToBpmn, declarativeToLayoutedBpmn, externalJobTypes, envelope, layoutBpmn, toBpmn, toDeployableBpmn, Worker, WorkflowClient, WorkflowError, } from "@nanobpm/workflow";
|
|
4
|
+
export { defineFlow, declarativeToBpmn, declarativeToLayoutedBpmn, externalJobTypes, inlineJobTypes, envelope, layoutBpmn, toBpmn, toDeployableBpmn, Worker, WorkflowClient, WorkflowError, } from "@nanobpm/workflow";
|
|
5
5
|
export type { FlowBuilder, TimerStart, Envelope, EnvelopeType, EnvelopeField, FieldSpec, ScalarType, WorkflowClientOptions, NanoSdkClient, NanoJobWorker, JobWorkerConfig, ActivatedJob, WorkerOptions, ActivityEvent, } from "@nanobpm/workflow";
|
package/dist/index.js
CHANGED
|
@@ -22,4 +22,4 @@ export { createCamundaClient } from "@nanobpm/nano-sdk";
|
|
|
22
22
|
// Toolkit — derivation library (`urban gen`)
|
|
23
23
|
export * from "./toolkit/index.js";
|
|
24
24
|
// Code-first process surface (re-exported from @nanobpm/workflow)
|
|
25
|
-
export { defineFlow, declarativeToBpmn, declarativeToLayoutedBpmn, externalJobTypes, envelope, layoutBpmn, toBpmn, toDeployableBpmn, Worker, WorkflowClient, WorkflowError, } from "@nanobpm/workflow";
|
|
25
|
+
export { defineFlow, declarativeToBpmn, declarativeToLayoutedBpmn, externalJobTypes, inlineJobTypes, envelope, layoutBpmn, toBpmn, toDeployableBpmn, Worker, WorkflowClient, WorkflowError, } from "@nanobpm/workflow";
|
package/dist/toolkit/gen.d.ts
CHANGED
|
@@ -48,6 +48,16 @@ export interface GenResult {
|
|
|
48
48
|
incomplete: boolean;
|
|
49
49
|
/** Per-file model-derivation errors (empty on success). */
|
|
50
50
|
modelErrors: ModelError[];
|
|
51
|
+
/** Job types of inline-handled (`w.run`) code-first steps — the set the app's in-process
|
|
52
|
+
* Worker already serves (nano-ide#595). The worker-stub scaffolder skips these; empty for a
|
|
53
|
+
* model-first app. */
|
|
54
|
+
inlineTaskTypes: string[];
|
|
55
|
+
/** The code-first models derived in-memory this run (`deriveModels().models`) — the
|
|
56
|
+
* worker-stub scaffolder plans against these in addition to the on-disk models, since a
|
|
57
|
+
* declared `models` block (e.g. `models.workflows`) suppresses the `resources/` convention
|
|
58
|
+
* scan that would otherwise pick the written `.bpmn` up (nano-ide#595). Empty for a
|
|
59
|
+
* model-first app. */
|
|
60
|
+
derivedModels: ModelSource[];
|
|
51
61
|
/** Non-fatal spec-hygiene warnings (e.g. a requestBody schema shared by >1 operation). Empty on
|
|
52
62
|
* a clean spec. Surfaced by the CLI but never fail gen/check — advisory only. */
|
|
53
63
|
warnings: string[];
|
package/dist/toolkit/gen.js
CHANGED
|
@@ -257,7 +257,7 @@ export async function runGen(opts) {
|
|
|
257
257
|
swept.push(rel);
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
-
return { artifacts, drift, swept: swept.sort(), incomplete: derived.incomplete, modelErrors: derived.errors, warnings };
|
|
260
|
+
return { artifacts, drift, swept: swept.sort(), incomplete: derived.incomplete, modelErrors: derived.errors, inlineTaskTypes: derived.inlineTaskTypes, derivedModels: derived.models, warnings };
|
|
261
261
|
}
|
|
262
262
|
export { dirOf, join as joinPath };
|
|
263
263
|
/** Derive code-first models in-memory (no writes) — backs `urban derive --stdout`, the
|
package/dist/toolkit/generate.js
CHANGED
|
@@ -30,7 +30,15 @@ export async function generate(opts) {
|
|
|
30
30
|
return empty;
|
|
31
31
|
const write = !opts.check;
|
|
32
32
|
const scaffoldOpts = { root: opts.root, io: opts.io, manifestFile: opts.manifestFile, write };
|
|
33
|
-
|
|
33
|
+
// Inline-handled (`w.run`) steps are already served by the app's in-process Worker — never
|
|
34
|
+
// scaffold a competing throwing stub for them (nano-ide#595). The derived models are also
|
|
35
|
+
// handed over: a code-first manifest's declared `models` block suppresses the resources/
|
|
36
|
+
// convention scan in readModels, so without them the scaffolder would plan from nothing.
|
|
37
|
+
const workers = await scaffoldWorkers({
|
|
38
|
+
...scaffoldOpts,
|
|
39
|
+
inlineTaskTypes: res.inlineTaskTypes,
|
|
40
|
+
extraModels: res.derivedModels,
|
|
41
|
+
});
|
|
34
42
|
const operations = await scaffoldOperations(scaffoldOpts);
|
|
35
43
|
const missingStubs = [
|
|
36
44
|
...workers.outcomes.filter((o) => o.status === "would-create").map((o) => o.handlerPath),
|
package/dist/toolkit/models.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ export interface DerivedModels {
|
|
|
38
38
|
models: ModelSource[];
|
|
39
39
|
/** Derived flow ids (used for the provenance-scoped stale sweep). */
|
|
40
40
|
ids: string[];
|
|
41
|
+
/** Job types of inline-handled (`w.run`) steps across every derived declarative flow —
|
|
42
|
+
* the set the app's in-process Worker already serves (nano-ide#595). The worker-stub
|
|
43
|
+
* scaffolder skips these; only `w.task` (external) steps may be scaffolded. Empty when
|
|
44
|
+
* derivation was a no-op (no loader / no workflow files) or all flows are imperative
|
|
45
|
+
* (an imperative flow's steps run inside its single `:__orchestrate` job, which the
|
|
46
|
+
* planner already skips). */
|
|
47
|
+
inlineTaskTypes: string[];
|
|
41
48
|
/** Per-flow `{ id, kind, xml }` for the non-writing preview (`urban derive --stdout`). */
|
|
42
49
|
list: DerivedModelInfo[];
|
|
43
50
|
/** The output directory the artifacts were written into. */
|
package/dist/toolkit/models.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// optional `GenIO.importModule` (present on Node with type-stripping, and on Deno). When the
|
|
9
9
|
// loader is absent — or the app authors its models directly (model-first) — derivation is a
|
|
10
10
|
// no-op and the rest of `urban gen` proceeds on the authored/derived models unchanged.
|
|
11
|
-
import { toDeployableBpmn } from "@nanobpm/workflow";
|
|
11
|
+
import { inlineJobTypes, toDeployableBpmn } from "@nanobpm/workflow";
|
|
12
12
|
import { expandPattern, joinPath } from "./gen.js";
|
|
13
13
|
import { isRecord } from "../runtime/core/guards.js";
|
|
14
14
|
/** Default source glob for code-first flows (matches the code-first scaffold layout). */
|
|
@@ -54,6 +54,7 @@ function emptyDerived(outDir) {
|
|
|
54
54
|
artifacts: [],
|
|
55
55
|
models: [],
|
|
56
56
|
ids: [],
|
|
57
|
+
inlineTaskTypes: [],
|
|
57
58
|
list: [],
|
|
58
59
|
outDir,
|
|
59
60
|
attempted: false,
|
|
@@ -131,6 +132,8 @@ export async function deriveModels(root, io, manifest) {
|
|
|
131
132
|
const path = `${outDir}/${bpmnFilename(id)}`;
|
|
132
133
|
out.artifacts.push({ path, content: xml });
|
|
133
134
|
out.models.push({ path, xml });
|
|
135
|
+
if (value.kind === "declarative")
|
|
136
|
+
out.inlineTaskTypes.push(...inlineJobTypes(value));
|
|
134
137
|
out.list.push({ id, kind: value.kind, xml });
|
|
135
138
|
out.ids.push(id);
|
|
136
139
|
}
|
|
@@ -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" | "external";
|
|
29
|
+
export type SkipReason = "already-wired" | "orchestrator" | "duplicate" | "external" | "inline";
|
|
30
30
|
export interface SkippedWorker {
|
|
31
31
|
taskType: string;
|
|
32
32
|
reason: SkipReason;
|
|
@@ -42,9 +42,12 @@ export declare function renderWorkerStub(taskType: string, typedIn: boolean, typ
|
|
|
42
42
|
/**
|
|
43
43
|
* Plan write-once handler stubs from the models. A stub is planned for each service-task
|
|
44
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),
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* declared external (serviced by an out-of-process worker), NOT inline-handled (a code-first
|
|
46
|
+
* `w.run` step whose handler the app's in-process Worker already serves — nano-ide#595), and
|
|
47
|
+
* not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
|
|
48
|
+
* envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
|
|
49
|
+
* `externalTaskTypes` are the manifest's declared external task types, which the app owns the
|
|
50
|
+
* model for but deliberately does not host. `inlineTaskTypes` are the flow-derived inline
|
|
51
|
+
* (`w.run`) job types (`deriveModels().inlineTaskTypes`).
|
|
49
52
|
*/
|
|
50
|
-
export declare function planWorkerScaffold(models: ModelSource[], workers?: ScaffoldWorker[], declaredTypeIds?: Iterable<string>, externalTaskTypes?: Iterable<string>): WorkerScaffoldPlan;
|
|
53
|
+
export declare function planWorkerScaffold(models: ModelSource[], workers?: ScaffoldWorker[], declaredTypeIds?: Iterable<string>, externalTaskTypes?: Iterable<string>, inlineTaskTypes?: Iterable<string>): WorkerScaffoldPlan;
|
|
@@ -65,17 +65,21 @@ export function renderWorkerStub(taskType, typedIn, typedOut) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Plan write-once handler stubs from the models. A stub is planned for each service-task
|
|
67
67
|
* `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, NOT
|
|
68
|
-
* declared external (serviced by an out-of-process worker),
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* declared external (serviced by an out-of-process worker), NOT inline-handled (a code-first
|
|
69
|
+
* `w.run` step whose handler the app's in-process Worker already serves — nano-ide#595), and
|
|
70
|
+
* not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
|
|
71
|
+
* envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
|
|
72
|
+
* `externalTaskTypes` are the manifest's declared external task types, which the app owns the
|
|
73
|
+
* model for but deliberately does not host. `inlineTaskTypes` are the flow-derived inline
|
|
74
|
+
* (`w.run`) job types (`deriveModels().inlineTaskTypes`).
|
|
72
75
|
*/
|
|
73
|
-
export function planWorkerScaffold(models, workers = [], declaredTypeIds = [], externalTaskTypes = []) {
|
|
76
|
+
export function planWorkerScaffold(models, workers = [], declaredTypeIds = [], externalTaskTypes = [], inlineTaskTypes = []) {
|
|
74
77
|
const wired = new Set(workers
|
|
75
78
|
.map((w) => w?.taskType)
|
|
76
79
|
.filter((t) => typeof t === "string" && t.length > 0));
|
|
77
80
|
const declared = new Set(declaredTypeIds);
|
|
78
81
|
const external = new Set(externalTaskTypes);
|
|
82
|
+
const inline = new Set(inlineTaskTypes);
|
|
79
83
|
const plans = [];
|
|
80
84
|
const skipped = [];
|
|
81
85
|
const seenTaskTypes = new Set();
|
|
@@ -91,6 +95,10 @@ export function planWorkerScaffold(models, workers = [], declaredTypeIds = [], e
|
|
|
91
95
|
skipped.push({ taskType, reason: "external" });
|
|
92
96
|
continue;
|
|
93
97
|
}
|
|
98
|
+
if (inline.has(taskType)) {
|
|
99
|
+
skipped.push({ taskType, reason: "inline" });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
94
102
|
if (wired.has(taskType)) {
|
|
95
103
|
skipped.push({ taskType, reason: "already-wired" });
|
|
96
104
|
continue;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GenIO } from "./gen.ts";
|
|
2
|
+
import type { ModelSource } from "./derivers/worker-io.ts";
|
|
2
3
|
import { type SkippedWorker, type StubManifestEntry } from "./scaffold/workers.ts";
|
|
3
4
|
/**
|
|
4
5
|
* Detect the indentation of a JSON document from its first indented line, so a rewrite can
|
|
@@ -18,6 +19,22 @@ export interface ScaffoldOptions {
|
|
|
18
19
|
manifestFile?: string;
|
|
19
20
|
/** Apply changes (create files + patch manifest). Default false = dry-run. */
|
|
20
21
|
write?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Job types of inline-handled (`w.run`) code-first steps — the set the app's in-process
|
|
24
|
+
* Worker already serves (nano-ide#595). The planner skips these with reason `"inline"` so
|
|
25
|
+
* `urban gen` never scaffolds a throwing stub that competes with the real handler. Pass
|
|
26
|
+
* `deriveModels().inlineTaskTypes` (the `generate` composition does).
|
|
27
|
+
*/
|
|
28
|
+
inlineTaskTypes?: Iterable<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Extra in-memory models to plan against in addition to the on-disk ones `readModels`
|
|
31
|
+
* resolves (nano-ide#595). A code-first app's derived `.bpmn` lives under
|
|
32
|
+
* `resources/processes/`, which `readModels` does NOT resolve when the manifest declares a
|
|
33
|
+
* `models` block (e.g. `models.workflows`) without `models.processes` — a declared block
|
|
34
|
+
* suppresses the `resources/` convention. Pass `deriveModels().models` so the scaffolder
|
|
35
|
+
* plans from the same fused model set the type-contract derivers saw.
|
|
36
|
+
*/
|
|
37
|
+
extraModels?: ModelSource[];
|
|
21
38
|
}
|
|
22
39
|
export type StubStatus = "created" | "would-create" | "kept";
|
|
23
40
|
export interface StubOutcome {
|
package/dist/toolkit/scaffold.js
CHANGED
|
@@ -38,9 +38,16 @@ export async function scaffoldWorkers(opts) {
|
|
|
38
38
|
// Preserve the manifest's own indentation when we patch it back (default tab, matching the
|
|
39
39
|
// scaffold's Biome config), so wiring a worker never reformats the file out from under lint.
|
|
40
40
|
const manifestIndent = detectJsonIndent(manifestRaw);
|
|
41
|
-
const
|
|
41
|
+
const diskModels = await readModels(root, io, manifest);
|
|
42
42
|
const declaredTypeIds = Object.keys(manifest.types ?? {});
|
|
43
|
-
|
|
43
|
+
// Fuse on-disk models with any in-memory extras (code-first derived models — see
|
|
44
|
+
// `ScaffoldOptions.extraModels`), deduped by path with the IN-MEMORY model winning: a derived
|
|
45
|
+
// model whose path also exists on disk may be stale there (`--check`/`--no-models` never write
|
|
46
|
+
// it), so planning from the disk copy would miss newly added tasks and keep removed ones. This
|
|
47
|
+
// mirrors the precedence `collectAll` (gen.ts) gives the type-contract derivers.
|
|
48
|
+
const extraPaths = new Set((opts.extraModels ?? []).map((m) => m.path));
|
|
49
|
+
const models = [...diskModels.filter((m) => !extraPaths.has(m.path)), ...(opts.extraModels ?? [])];
|
|
50
|
+
const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds, manifest.externalTaskTypes ?? [], opts.inlineTaskTypes ?? []);
|
|
44
51
|
const outcomes = [];
|
|
45
52
|
const wired = [];
|
|
46
53
|
for (const plan of plans) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.96.
|
|
3
|
+
"version": "0.96.3",
|
|
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",
|
|
@@ -106,9 +106,9 @@
|
|
|
106
106
|
"@nanobpm/agentic": "^0.15.0",
|
|
107
107
|
"@nanobpm/nano-app-schema": "^0.11.0",
|
|
108
108
|
"@nanobpm/nano-sdk": "^1.2.5",
|
|
109
|
-
"@nanobpm/workflow": "^0.15.
|
|
109
|
+
"@nanobpm/workflow": "^0.15.1",
|
|
110
110
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
111
|
-
"create-urban-app": "^0.15.
|
|
111
|
+
"create-urban-app": "^0.15.3",
|
|
112
112
|
"yaml": "^2.9.0"
|
|
113
113
|
},
|
|
114
114
|
"devDependencies": {
|
package/src/index.ts
CHANGED
package/src/toolkit/gen.ts
CHANGED
|
@@ -65,6 +65,16 @@ export interface GenResult {
|
|
|
65
65
|
incomplete: boolean;
|
|
66
66
|
/** Per-file model-derivation errors (empty on success). */
|
|
67
67
|
modelErrors: ModelError[];
|
|
68
|
+
/** Job types of inline-handled (`w.run`) code-first steps — the set the app's in-process
|
|
69
|
+
* Worker already serves (nano-ide#595). The worker-stub scaffolder skips these; empty for a
|
|
70
|
+
* model-first app. */
|
|
71
|
+
inlineTaskTypes: string[];
|
|
72
|
+
/** The code-first models derived in-memory this run (`deriveModels().models`) — the
|
|
73
|
+
* worker-stub scaffolder plans against these in addition to the on-disk models, since a
|
|
74
|
+
* declared `models` block (e.g. `models.workflows`) suppresses the `resources/` convention
|
|
75
|
+
* scan that would otherwise pick the written `.bpmn` up (nano-ide#595). Empty for a
|
|
76
|
+
* model-first app. */
|
|
77
|
+
derivedModels: ModelSource[];
|
|
68
78
|
/** Non-fatal spec-hygiene warnings (e.g. a requestBody schema shared by >1 operation). Empty on
|
|
69
79
|
* a clean spec. Surfaced by the CLI but never fail gen/check — advisory only. */
|
|
70
80
|
warnings: string[];
|
|
@@ -333,7 +343,7 @@ export async function runGen(opts: GenOptions & { check?: boolean }): Promise<Ge
|
|
|
333
343
|
}
|
|
334
344
|
}
|
|
335
345
|
|
|
336
|
-
return { artifacts, drift, swept: swept.sort(), incomplete: derived.incomplete, modelErrors: derived.errors, warnings };
|
|
346
|
+
return { artifacts, drift, swept: swept.sort(), incomplete: derived.incomplete, modelErrors: derived.errors, inlineTaskTypes: derived.inlineTaskTypes, derivedModels: derived.models, warnings };
|
|
337
347
|
}
|
|
338
348
|
|
|
339
349
|
export { dirOf, join as joinPath };
|
|
@@ -2,9 +2,13 @@ import { test } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { generate } from "./generate.ts";
|
|
4
4
|
import type { GenIO } from "./gen.ts";
|
|
5
|
+
import { defineFlow, envelope } from "@nanobpm/workflow";
|
|
5
6
|
|
|
6
7
|
/** In-memory filesystem mirroring gen.test.ts's memIO (IO-free, deterministic). */
|
|
7
|
-
function memIO(
|
|
8
|
+
function memIO(
|
|
9
|
+
files: Record<string, string>,
|
|
10
|
+
modules: Record<string, Record<string, unknown>> = {},
|
|
11
|
+
): GenIO & { files: Record<string, string> } {
|
|
8
12
|
return {
|
|
9
13
|
files,
|
|
10
14
|
async readText(p) {
|
|
@@ -25,12 +29,28 @@ function memIO(files: Record<string, string>): GenIO & { files: Record<string, s
|
|
|
25
29
|
}
|
|
26
30
|
return [...names];
|
|
27
31
|
},
|
|
32
|
+
async listSubdirs(p) {
|
|
33
|
+
const prefix = p.replace(/\/+$/, "") + "/";
|
|
34
|
+
const names = new Set<string>();
|
|
35
|
+
for (const f of Object.keys(files)) {
|
|
36
|
+
if (f.startsWith(prefix)) {
|
|
37
|
+
const rest = f.slice(prefix.length);
|
|
38
|
+
const slash = rest.indexOf("/");
|
|
39
|
+
if (slash > 0) names.add(rest.slice(0, slash));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...names];
|
|
43
|
+
},
|
|
28
44
|
async exists(p) {
|
|
29
45
|
return p in files;
|
|
30
46
|
},
|
|
31
47
|
async remove(p) {
|
|
32
48
|
delete files[p];
|
|
33
49
|
},
|
|
50
|
+
async importModule(p) {
|
|
51
|
+
if (!(p in modules)) throw new Error(`no module registered for ${p}`);
|
|
52
|
+
return modules[p];
|
|
53
|
+
},
|
|
34
54
|
};
|
|
35
55
|
}
|
|
36
56
|
|
|
@@ -133,3 +153,68 @@ test("generate keeps existing human-owned stubs (write-once, never clobbered)",
|
|
|
133
153
|
assert.deepEqual(res.operationStubs.map((s) => s.status), ["kept"]);
|
|
134
154
|
assert.deepEqual(res.missingStubs, []);
|
|
135
155
|
});
|
|
156
|
+
|
|
157
|
+
// --- code-first: w.run steps are inline-handled, so gen must never scaffold stubs for them ---
|
|
158
|
+
|
|
159
|
+
const GreetIn = envelope("GreetIn", { who: "string" });
|
|
160
|
+
const GreetOut = envelope("GreetOut", { message: "string" });
|
|
161
|
+
|
|
162
|
+
/** The stock code-first template's flow: one inline-handled `w.run` step (nano-ide#595's repro). */
|
|
163
|
+
const greetFlow = defineFlow("greet", { hello: { in: GreetIn, out: GreetOut } }, (w) => {
|
|
164
|
+
w.run("hello", async () => ({ message: "hi" }));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("generate on a code-first app scaffolds NO stub for an inline w.run step (nano-ide#595)", async () => {
|
|
168
|
+
// The stock code-first app (create-urban-app --code-first): `greet:hello` is served by
|
|
169
|
+
// `w.run("hello", …)` and hosted in-process by main.ts's Worker. Scaffolding a throwing stub
|
|
170
|
+
// for it puts a second, always-failing worker on the same job type.
|
|
171
|
+
const io = memIO(
|
|
172
|
+
{
|
|
173
|
+
"/cf/nano.app.json": JSON.stringify({
|
|
174
|
+
id: "cf",
|
|
175
|
+
data: { default: "app" },
|
|
176
|
+
models: { workflows: ["workflows/*.ts"] },
|
|
177
|
+
}),
|
|
178
|
+
"/cf/workflows/greet.ts": "// derived in-memory via importModule stub",
|
|
179
|
+
},
|
|
180
|
+
{ "/cf/workflows/greet.ts": { greet: greetFlow } },
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const res = await generate({ root: "/cf", io });
|
|
184
|
+
|
|
185
|
+
assert.equal(res.incomplete, false);
|
|
186
|
+
assert.deepEqual(res.workerStubs, [], "no worker stub is scaffolded for an inline-handled step");
|
|
187
|
+
assert.deepEqual(res.wiredWorkers, [], "nothing is wired into workers[]");
|
|
188
|
+
assert.equal(res.manifestPatched, false, "the manifest is untouched");
|
|
189
|
+
assert.equal(io.files["/cf/workers/greet-hello/worker.ts"], undefined, "no throwing stub on disk");
|
|
190
|
+
const manifest = JSON.parse(io.files["/cf/nano.app.json"]);
|
|
191
|
+
assert.equal(manifest.workers, undefined, "workers[] stays undeclared");
|
|
192
|
+
// The model itself is still derived and written.
|
|
193
|
+
assert.ok(io.files["/cf/resources/processes/greet.bpmn"], "the derived .bpmn still lands");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("generate on a code-first app still scaffolds a stub for an external w.task step", async () => {
|
|
197
|
+
// The other half of the split: only `w.run` steps are inline; a `w.task` step has no in-process
|
|
198
|
+
// handler, so it still needs its write-once stub.
|
|
199
|
+
const flow = defineFlow("orders", (w) => {
|
|
200
|
+
w.run("charge", async () => ({}));
|
|
201
|
+
w.task("sign"); // external worker
|
|
202
|
+
});
|
|
203
|
+
const io = memIO(
|
|
204
|
+
{
|
|
205
|
+
"/cf/nano.app.json": JSON.stringify({
|
|
206
|
+
id: "cf",
|
|
207
|
+
data: { default: "app" },
|
|
208
|
+
models: { workflows: ["workflows/*.ts"] },
|
|
209
|
+
}),
|
|
210
|
+
"/cf/workflows/orders.ts": "// derived in-memory via importModule stub",
|
|
211
|
+
},
|
|
212
|
+
{ "/cf/workflows/orders.ts": { flow } },
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const res = await generate({ root: "/cf", io });
|
|
216
|
+
|
|
217
|
+
assert.deepEqual(res.workerStubs.map((s) => s.taskType), ["orders:sign"]);
|
|
218
|
+
assert.ok(io.files["/cf/workers/orders-sign/worker.ts"], "external task gets its stub");
|
|
219
|
+
assert.equal(io.files["/cf/workers/orders-charge/worker.ts"], undefined, "inline step gets none");
|
|
220
|
+
});
|
package/src/toolkit/generate.ts
CHANGED
|
@@ -52,7 +52,15 @@ export async function generate(opts: GenOptions & { check?: boolean }): Promise<
|
|
|
52
52
|
|
|
53
53
|
const write = !opts.check;
|
|
54
54
|
const scaffoldOpts = { root: opts.root, io: opts.io, manifestFile: opts.manifestFile, write };
|
|
55
|
-
|
|
55
|
+
// Inline-handled (`w.run`) steps are already served by the app's in-process Worker — never
|
|
56
|
+
// scaffold a competing throwing stub for them (nano-ide#595). The derived models are also
|
|
57
|
+
// handed over: a code-first manifest's declared `models` block suppresses the resources/
|
|
58
|
+
// convention scan in readModels, so without them the scaffolder would plan from nothing.
|
|
59
|
+
const workers = await scaffoldWorkers({
|
|
60
|
+
...scaffoldOpts,
|
|
61
|
+
inlineTaskTypes: res.inlineTaskTypes,
|
|
62
|
+
extraModels: res.derivedModels,
|
|
63
|
+
});
|
|
56
64
|
const operations = await scaffoldOperations(scaffoldOpts);
|
|
57
65
|
|
|
58
66
|
const missingStubs = [
|
|
@@ -157,3 +157,24 @@ test("deriveModels treats a genuine same-module re-export as one model, no error
|
|
|
157
157
|
assert.deepEqual(d.errors, []);
|
|
158
158
|
assert.equal(d.ids.length, 1);
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
test("deriveModels reports the inline-handled (w.run) task types alongside the model (nano-ide#595)", async () => {
|
|
162
|
+
// The worker-stub scaffolder consumes `inlineTaskTypes` to skip steps whose handler is hosted
|
|
163
|
+
// in-process by the flow's Worker — only `w.task` (external) steps may be scaffolded.
|
|
164
|
+
const mixed = defineFlow("orders", (w) => {
|
|
165
|
+
w.run("charge", async () => ({}));
|
|
166
|
+
w.task("sign"); // external — must NOT be reported inline
|
|
167
|
+
w.run("ship", async () => ({}));
|
|
168
|
+
});
|
|
169
|
+
const io = memIO(
|
|
170
|
+
{ "/app/workflows/orders.ts": "// source" },
|
|
171
|
+
{ "workflows/orders.ts": { mixed } },
|
|
172
|
+
);
|
|
173
|
+
const d = await deriveModels("/app", io, CODE_FIRST_MANIFEST);
|
|
174
|
+
assert.equal(d.incomplete, false);
|
|
175
|
+
assert.deepEqual(d.inlineTaskTypes.sort(), ["orders:charge", "orders:ship"]);
|
|
176
|
+
// A no-op derivation (no loader / no workflow files) reports an empty set, not undefined.
|
|
177
|
+
const noLoader = memIO({ "/app/workflows/orders.ts": "// source" });
|
|
178
|
+
delete noLoader.importModule;
|
|
179
|
+
assert.deepEqual((await deriveModels("/app", noLoader, CODE_FIRST_MANIFEST)).inlineTaskTypes, []);
|
|
180
|
+
});
|
package/src/toolkit/models.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// loader is absent — or the app authors its models directly (model-first) — derivation is a
|
|
10
10
|
// no-op and the rest of `urban gen` proceeds on the authored/derived models unchanged.
|
|
11
11
|
|
|
12
|
-
import { toDeployableBpmn } from "@nanobpm/workflow";
|
|
12
|
+
import { inlineJobTypes, toDeployableBpmn } from "@nanobpm/workflow";
|
|
13
13
|
import type { Workflow } from "@nanobpm/workflow";
|
|
14
14
|
import type { DerivedArtifact } from "./artifact.ts";
|
|
15
15
|
import type { GenIO } from "./gen.ts";
|
|
@@ -78,6 +78,13 @@ export interface DerivedModels {
|
|
|
78
78
|
models: ModelSource[];
|
|
79
79
|
/** Derived flow ids (used for the provenance-scoped stale sweep). */
|
|
80
80
|
ids: string[];
|
|
81
|
+
/** Job types of inline-handled (`w.run`) steps across every derived declarative flow —
|
|
82
|
+
* the set the app's in-process Worker already serves (nano-ide#595). The worker-stub
|
|
83
|
+
* scaffolder skips these; only `w.task` (external) steps may be scaffolded. Empty when
|
|
84
|
+
* derivation was a no-op (no loader / no workflow files) or all flows are imperative
|
|
85
|
+
* (an imperative flow's steps run inside its single `:__orchestrate` job, which the
|
|
86
|
+
* planner already skips). */
|
|
87
|
+
inlineTaskTypes: string[];
|
|
81
88
|
/** Per-flow `{ id, kind, xml }` for the non-writing preview (`urban derive --stdout`). */
|
|
82
89
|
list: DerivedModelInfo[];
|
|
83
90
|
/** The output directory the artifacts were written into. */
|
|
@@ -101,6 +108,7 @@ function emptyDerived(outDir: string): DerivedModels {
|
|
|
101
108
|
artifacts: [],
|
|
102
109
|
models: [],
|
|
103
110
|
ids: [],
|
|
111
|
+
inlineTaskTypes: [],
|
|
104
112
|
list: [],
|
|
105
113
|
outDir,
|
|
106
114
|
attempted: false,
|
|
@@ -184,6 +192,7 @@ export async function deriveModels(
|
|
|
184
192
|
const path = `${outDir}/${bpmnFilename(id)}`;
|
|
185
193
|
out.artifacts.push({ path, content: xml });
|
|
186
194
|
out.models.push({ path, xml });
|
|
195
|
+
if (value.kind === "declarative") out.inlineTaskTypes.push(...inlineJobTypes(value));
|
|
187
196
|
out.list.push({ id, kind: value.kind, xml });
|
|
188
197
|
out.ids.push(id);
|
|
189
198
|
} catch (e) {
|
|
@@ -146,6 +146,19 @@ test("planWorkerScaffold: skips a task type declared external (out-of-process wo
|
|
|
146
146
|
assert.equal(reasons["senior:review"], "external");
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test("planWorkerScaffold: skips an inline-handled (w.run) task type — never stubs a hosted handler (nano-ide#595)", () => {
|
|
150
|
+
// A code-first `w.run` step's handler lives inline in the flow and is hosted by the in-process
|
|
151
|
+
// Worker; scaffolding a throwing stub for its job type would put a second, always-failing worker
|
|
152
|
+
// on the same job type. The derivation reports those types, and the planner must skip them.
|
|
153
|
+
const models = [
|
|
154
|
+
model("a.bpmn", "greet", serviceTask("hello", "greet:hello"), serviceTask("B", "greet:ship")),
|
|
155
|
+
];
|
|
156
|
+
const { plans, skipped } = planWorkerScaffold(models, [], [], [], ["greet:hello"]);
|
|
157
|
+
assert.deepEqual(plans.map((p) => p.taskType), ["greet:ship"]);
|
|
158
|
+
const reasons = Object.fromEntries(skipped.map((s) => [s.taskType, s.reason]));
|
|
159
|
+
assert.equal(reasons["greet:hello"], "inline");
|
|
160
|
+
});
|
|
161
|
+
|
|
149
162
|
test("planWorkerScaffold: colliding slugs get a numeric suffix", () => {
|
|
150
163
|
const m = model("a.bpmn", "p", serviceTask("A", "orders.charge"), serviceTask("B", "orders-charge"));
|
|
151
164
|
const { plans } = planWorkerScaffold([m], [], []);
|
|
@@ -258,6 +271,58 @@ test("scaffoldWorkers is idempotent: a second --write run is a no-op", async ()
|
|
|
258
271
|
assert.equal(JSON.stringify(io.files), snapshot, "files unchanged on re-run");
|
|
259
272
|
});
|
|
260
273
|
|
|
274
|
+
test("scaffoldWorkers skips inline-handled (w.run) task types — no stub, no wiring (nano-ide#595)", async () => {
|
|
275
|
+
const files = {
|
|
276
|
+
"/app/nano.app.json": JSON.stringify({
|
|
277
|
+
id: "demo",
|
|
278
|
+
models: { processes: ["processes/*.bpmn"] },
|
|
279
|
+
}),
|
|
280
|
+
"/app/processes/p.bpmn": model("p.bpmn", "greet",
|
|
281
|
+
serviceTask("hello", "greet:hello"),
|
|
282
|
+
serviceTask("B", "greet:notify"),
|
|
283
|
+
).xml,
|
|
284
|
+
};
|
|
285
|
+
const io = memIO(files);
|
|
286
|
+
const run = await scaffoldWorkers({ root: "/app", io, write: true, inlineTaskTypes: ["greet:hello"] });
|
|
287
|
+
// Only the external (un-hosted) task is scaffolded; the inline one is skipped entirely.
|
|
288
|
+
assert.deepEqual(run.outcomes.map((o) => o.taskType), ["greet:notify"]);
|
|
289
|
+
assert.ok(run.skipped.some((s) => s.taskType === "greet:hello" && s.reason === "inline"));
|
|
290
|
+
assert.equal(io.files["/app/workers/greet-hello/worker.ts"], undefined, "inline task gets no stub");
|
|
291
|
+
const manifest = JSON.parse(io.files["/app/nano.app.json"]);
|
|
292
|
+
assert.ok(
|
|
293
|
+
!manifest.workers?.some((w: { taskType: string }) => w.taskType === "greet:hello"),
|
|
294
|
+
"inline task is never wired into workers[]",
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("scaffoldWorkers prefers an in-memory derived model over a stale disk copy at the same path (nano-ide#595)", async () => {
|
|
299
|
+
// `--check`/`--no-models` runs never write the derived `.bpmn`, so a derived model's path can
|
|
300
|
+
// hold a STALE disk copy. Planning from disk would miss newly added external tasks and keep
|
|
301
|
+
// removed ones. The in-memory model must win — the same precedence `collectAll` (gen.ts) gives
|
|
302
|
+
// the type-contract derivers.
|
|
303
|
+
const files = {
|
|
304
|
+
"/app/nano.app.json": JSON.stringify({
|
|
305
|
+
id: "demo",
|
|
306
|
+
models: { processes: ["processes/*.bpmn"] },
|
|
307
|
+
}),
|
|
308
|
+
// Stale disk copy: still has the REMOVED task, lacks the ADDED one.
|
|
309
|
+
"/app/processes/p.bpmn": model("p.bpmn", "p", serviceTask("Old", "t.removed")).xml,
|
|
310
|
+
};
|
|
311
|
+
const io = memIO(files);
|
|
312
|
+
const derived = model("processes/p.bpmn", "p", serviceTask("New", "t.added"));
|
|
313
|
+
const run = await scaffoldWorkers({ root: "/app", io, write: true, extraModels: [derived] });
|
|
314
|
+
// The fresh model is planned from: the added task is scaffolded, the removed one is not.
|
|
315
|
+
assert.deepEqual(run.outcomes.map((o) => o.taskType), ["t.added"]);
|
|
316
|
+
assert.ok(io.files["/app/workers/t-added/worker.ts"], "added task gets a stub");
|
|
317
|
+
assert.equal(io.files["/app/workers/t-removed/worker.ts"], undefined, "removed task gets no stub");
|
|
318
|
+
const manifest = JSON.parse(io.files["/app/nano.app.json"]);
|
|
319
|
+
assert.deepEqual(
|
|
320
|
+
manifest.workers?.map((w: { taskType: string }) => w.taskType),
|
|
321
|
+
["t.added"],
|
|
322
|
+
"only the fresh model's task is wired",
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
261
326
|
test("scaffoldWorkers reads manifest.externalTaskTypes and skips those (never scaffolds/wires)", async () => {
|
|
262
327
|
const files = {
|
|
263
328
|
"/app/nano.app.json": JSON.stringify({
|
|
@@ -39,7 +39,7 @@ export interface WorkerStubPlan {
|
|
|
39
39
|
manifestEntry: StubManifestEntry;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export type SkipReason = "already-wired" | "orchestrator" | "duplicate" | "external";
|
|
42
|
+
export type SkipReason = "already-wired" | "orchestrator" | "duplicate" | "external" | "inline";
|
|
43
43
|
|
|
44
44
|
export interface SkippedWorker {
|
|
45
45
|
taskType: string;
|
|
@@ -118,16 +118,20 @@ export function renderWorkerStub(
|
|
|
118
118
|
/**
|
|
119
119
|
* Plan write-once handler stubs from the models. A stub is planned for each service-task
|
|
120
120
|
* `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, NOT
|
|
121
|
-
* declared external (serviced by an out-of-process worker),
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
121
|
+
* declared external (serviced by an out-of-process worker), NOT inline-handled (a code-first
|
|
122
|
+
* `w.run` step whose handler the app's in-process Worker already serves — nano-ide#595), and
|
|
123
|
+
* not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
|
|
124
|
+
* envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
|
|
125
|
+
* `externalTaskTypes` are the manifest's declared external task types, which the app owns the
|
|
126
|
+
* model for but deliberately does not host. `inlineTaskTypes` are the flow-derived inline
|
|
127
|
+
* (`w.run`) job types (`deriveModels().inlineTaskTypes`).
|
|
125
128
|
*/
|
|
126
129
|
export function planWorkerScaffold(
|
|
127
130
|
models: ModelSource[],
|
|
128
131
|
workers: ScaffoldWorker[] = [],
|
|
129
132
|
declaredTypeIds: Iterable<string> = [],
|
|
130
133
|
externalTaskTypes: Iterable<string> = [],
|
|
134
|
+
inlineTaskTypes: Iterable<string> = [],
|
|
131
135
|
): WorkerScaffoldPlan {
|
|
132
136
|
const wired = new Set(
|
|
133
137
|
workers
|
|
@@ -136,6 +140,7 @@ export function planWorkerScaffold(
|
|
|
136
140
|
);
|
|
137
141
|
const declared = new Set(declaredTypeIds);
|
|
138
142
|
const external = new Set(externalTaskTypes);
|
|
143
|
+
const inline = new Set(inlineTaskTypes);
|
|
139
144
|
|
|
140
145
|
const plans: WorkerStubPlan[] = [];
|
|
141
146
|
const skipped: SkippedWorker[] = [];
|
|
@@ -153,6 +158,10 @@ export function planWorkerScaffold(
|
|
|
153
158
|
skipped.push({ taskType, reason: "external" });
|
|
154
159
|
continue;
|
|
155
160
|
}
|
|
161
|
+
if (inline.has(taskType)) {
|
|
162
|
+
skipped.push({ taskType, reason: "inline" });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
156
165
|
if (wired.has(taskType)) {
|
|
157
166
|
skipped.push({ taskType, reason: "already-wired" });
|
|
158
167
|
continue;
|
package/src/toolkit/scaffold.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { GenIO } from "./gen.ts";
|
|
7
7
|
import { joinPath, readModels } from "./gen.ts";
|
|
8
|
+
import type { ModelSource } from "./derivers/worker-io.ts";
|
|
8
9
|
import {
|
|
9
10
|
planWorkerScaffold,
|
|
10
11
|
type ScaffoldWorker,
|
|
@@ -50,6 +51,22 @@ export interface ScaffoldOptions {
|
|
|
50
51
|
manifestFile?: string;
|
|
51
52
|
/** Apply changes (create files + patch manifest). Default false = dry-run. */
|
|
52
53
|
write?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Job types of inline-handled (`w.run`) code-first steps — the set the app's in-process
|
|
56
|
+
* Worker already serves (nano-ide#595). The planner skips these with reason `"inline"` so
|
|
57
|
+
* `urban gen` never scaffolds a throwing stub that competes with the real handler. Pass
|
|
58
|
+
* `deriveModels().inlineTaskTypes` (the `generate` composition does).
|
|
59
|
+
*/
|
|
60
|
+
inlineTaskTypes?: Iterable<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Extra in-memory models to plan against in addition to the on-disk ones `readModels`
|
|
63
|
+
* resolves (nano-ide#595). A code-first app's derived `.bpmn` lives under
|
|
64
|
+
* `resources/processes/`, which `readModels` does NOT resolve when the manifest declares a
|
|
65
|
+
* `models` block (e.g. `models.workflows`) without `models.processes` — a declared block
|
|
66
|
+
* suppresses the `resources/` convention. Pass `deriveModels().models` so the scaffolder
|
|
67
|
+
* plans from the same fused model set the type-contract derivers saw.
|
|
68
|
+
*/
|
|
69
|
+
extraModels?: ModelSource[];
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
export type StubStatus = "created" | "would-create" | "kept";
|
|
@@ -87,14 +104,23 @@ export async function scaffoldWorkers(opts: ScaffoldOptions): Promise<ScaffoldRu
|
|
|
87
104
|
// Preserve the manifest's own indentation when we patch it back (default tab, matching the
|
|
88
105
|
// scaffold's Biome config), so wiring a worker never reformats the file out from under lint.
|
|
89
106
|
const manifestIndent = detectJsonIndent(manifestRaw);
|
|
90
|
-
const
|
|
107
|
+
const diskModels = await readModels(root, io, manifest);
|
|
91
108
|
const declaredTypeIds = Object.keys(manifest.types ?? {});
|
|
92
109
|
|
|
110
|
+
// Fuse on-disk models with any in-memory extras (code-first derived models — see
|
|
111
|
+
// `ScaffoldOptions.extraModels`), deduped by path with the IN-MEMORY model winning: a derived
|
|
112
|
+
// model whose path also exists on disk may be stale there (`--check`/`--no-models` never write
|
|
113
|
+
// it), so planning from the disk copy would miss newly added tasks and keep removed ones. This
|
|
114
|
+
// mirrors the precedence `collectAll` (gen.ts) gives the type-contract derivers.
|
|
115
|
+
const extraPaths = new Set((opts.extraModels ?? []).map((m) => m.path));
|
|
116
|
+
const models = [...diskModels.filter((m) => !extraPaths.has(m.path)), ...(opts.extraModels ?? [])];
|
|
117
|
+
|
|
93
118
|
const { plans, skipped } = planWorkerScaffold(
|
|
94
119
|
models,
|
|
95
120
|
manifest.workers ?? [],
|
|
96
121
|
declaredTypeIds,
|
|
97
122
|
manifest.externalTaskTypes ?? [],
|
|
123
|
+
opts.inlineTaskTypes ?? [],
|
|
98
124
|
);
|
|
99
125
|
|
|
100
126
|
const outcomes: StubOutcome[] = [];
|