@nanobpm/urban 0.96.1 → 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/cli.js CHANGED
@@ -11,11 +11,15 @@
11
11
  //
12
12
  // Global flags: --root <dir> (default "."), --manifest <file> (default nano.app.json),
13
13
  // --port <n>, -h/--help, -v/--version.
14
- import { collectManifestIssues, installSignalHandlers, loadManifest, runDataOp, runDev, runFromEnv, selectHost, } from "./runtime/index.js";
14
+ //
15
+ // Module-load laziness (nano-ide#592): only argument parsing, the stdio/guard helpers and the
16
+ // dispatcher load eagerly. Every command's modules are dynamically imported inside its handler —
17
+ // a top-level import of the toolkit, the scaffold package or the runtime barrel made EVERY
18
+ // command (including the per-data-op `urban data` gateway) pay the full ~2s load on small
19
+ // hosts before dispatching. `urban data` loads only the host detector and the data-op runner.
20
+ // Guarded by cli-lazy.test.ts, which records the URLs each command resolves.
15
21
  import { denoGlobal, processGlobal } from "./runtime/adapters/globals.js";
16
22
  import { errorMessage, isRecord } from "./runtime/core/guards.js";
17
- import { scaffold, slugify } from "create-urban-app";
18
- import { addConnector, createNodeGenIO, previewModels, generate, runGen } from "./toolkit/index.js";
19
23
  import { pathToFileURL } from "node:url";
20
24
  import { readFileSync } from "node:fs";
21
25
  import { spawnSync } from "node:child_process";
@@ -119,6 +123,9 @@ Global flags:
119
123
  Engine address: $CAMUNDA_REST_ADDRESS (default http://localhost:8080/v2).
120
124
  `;
121
125
  async function cmdCheck(f) {
126
+ const { selectHost } = await import("./runtime/adapters/detect.js");
127
+ const { loadManifest } = await import("./runtime/core/manifest.js");
128
+ const { collectManifestIssues } = await import("./runtime/core/validate.js");
122
129
  // The host is anchored at f.root, so manifest paths are already root-relative —
123
130
  // pass the bare manifest filename (prefixing root again would double it).
124
131
  const host = selectHost({ cwd: f.root });
@@ -134,6 +141,7 @@ async function cmdCheck(f) {
134
141
  return 1;
135
142
  }
136
143
  async function cmdGen(f) {
144
+ const { createNodeGenIO, generate } = await import("./toolkit/index.js");
137
145
  const io = createNodeGenIO();
138
146
  const res = await generate({ root: f.root, io, manifestFile: f.manifest, check: f.check, emitModels: f.models });
139
147
  if (res.incomplete) {
@@ -198,6 +206,7 @@ async function cmdDerive(f) {
198
206
  console.error("✖ `urban derive` derives models — `--no-models` is contradictory. Use `urban gen --no-models` for type-contracts only.");
199
207
  return 2;
200
208
  }
209
+ const { createNodeGenIO, previewModels, runGen } = await import("./toolkit/index.js");
201
210
  const io = createNodeGenIO();
202
211
  // Non-writing preview: emit `{ models: [{ id, kind, xml }], incomplete }` to stdout for the
203
212
  // console's read-only model viewer (never touches `processes/`).
@@ -238,6 +247,7 @@ async function cmdDerive(f) {
238
247
  return 0;
239
248
  }
240
249
  async function cmdRun(f, mount) {
250
+ const { runFromEnv } = await import("./runtime/run.js");
241
251
  const app = await runFromEnv({
242
252
  root: f.root,
243
253
  manifestPath: f.manifest,
@@ -249,11 +259,14 @@ async function cmdRun(f, mount) {
249
259
  return -1; // keep the process alive; signal handlers stop it
250
260
  }
251
261
  async function cmdDev(f) {
262
+ const { runDev } = await import("./runtime/devserver.js");
263
+ const { installSignalHandlers } = await import("./runtime/run.js");
252
264
  const dev = await runDev({ root: f.root, manifestPath: f.manifest, port: f.port });
253
265
  installSignalHandlers(() => dev.stop());
254
266
  return -1; // keep the process alive; signal handlers stop the dev server
255
267
  }
256
268
  async function cmdDeploy(f) {
269
+ const { runFromEnv } = await import("./runtime/run.js");
257
270
  const app = await runFromEnv({
258
271
  root: f.root,
259
272
  manifestPath: f.manifest,
@@ -306,6 +319,12 @@ export async function cmdData(f, readInput = readStdin) {
306
319
  }));
307
320
  return 0;
308
321
  }
322
+ // Lazily the only modules `urban data` loads (nano-ide#592): the host detector (both
323
+ // adapters, no toolkit) and the data-op runner (gateway + SQLite adapter; the toolkit
324
+ // derivers behind the `domaintypes` op load only when that op is dispatched). Imported
325
+ // directly rather than via the runtime barrel, which would pull in the whole runtime.
326
+ const { selectHost } = await import("./runtime/adapters/detect.js");
327
+ const { runDataOp } = await import("./runtime/core/modules/dataops.js");
309
328
  const host = selectHost({ cwd: f.root });
310
329
  try {
311
330
  // Mirror runFromEnv: the host is anchored at f.root, so pass root "." to avoid
@@ -324,6 +343,7 @@ async function cmdNew(f) {
324
343
  console.error("usage: urban new <name> [--root <path>] [--deno] [--style model|code] (or --code-first)");
325
344
  return 1;
326
345
  }
346
+ const { scaffold, slugify } = await import("create-urban-app");
327
347
  const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
328
348
  const res = await scaffold({ name, dir, deno: f.deno, style: f.style });
329
349
  console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
@@ -362,6 +382,7 @@ async function cmdAdd(f) {
362
382
  }
363
383
  }
364
384
  try {
385
+ const { addConnector, createNodeGenIO } = await import("./toolkit/index.js");
365
386
  const result = await addConnector({
366
387
  root: f.root,
367
388
  pkg,
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";
@@ -1,9 +1,9 @@
1
1
  import type { HostContext } from "../host.ts";
2
2
  import type { AppManifest } from "../manifest.ts";
3
- import { type ShapeDecl } from "../../../toolkit/derivers/shapes.ts";
4
- import { type WorkerBindingDecl } from "../../../toolkit/derivers/worker-io.ts";
5
- import { type MessageBindingDecl } from "../../../toolkit/derivers/messages.ts";
6
- import { type MetaDecl } from "../../../toolkit/derivers/meta.ts";
3
+ import type { ShapeDecl } from "../../../toolkit/derivers/shapes.ts";
4
+ import type { WorkerBindingDecl } from "../../../toolkit/derivers/worker-io.ts";
5
+ import type { MessageBindingDecl } from "../../../toolkit/derivers/messages.ts";
6
+ import type { MetaDecl } from "../../../toolkit/derivers/meta.ts";
7
7
  /** The DB-manager op protocol's known op set — every op (including `domaintypes`) is dispatched by
8
8
  * `runDataOp`. Typing `op` as this union (instead of `string`) gives SDK/console callers type-safety
9
9
  * and catches typos at compile time. */
@@ -23,16 +23,6 @@ import { loadManifest } from "../manifest.js";
23
23
  import { validateManifest } from "../validate.js";
24
24
  import { makeGateway } from "./gateway.js";
25
25
  import { applyMigrations, MIGRATIONS_TABLE, openSqliteSource, resolveAppPath } from "./datasource.js";
26
- // The `domaintypes` op composes the domain reifier from the pure toolkit derivers (single source of
27
- // truth for the IDE's codegen, host dry-out nano-bpm#576). Imported directly from the deriver files
28
- // (not the `toolkit/` barrel, which pulls the Node fs IO) so core stays free of `node:*`/Deno; the
29
- // derivers' only core dependency is the gateway's `TableMeta`/`ColumnMeta` *types*, which erase.
30
- import { GENERATED_DIR } from "../../../toolkit/artifact.js";
31
- import { DOMAIN_BINDINGS, emitDomainBindings, emitDomainModel, registryFromManifest, } from "../../../toolkit/derivers/domain.js";
32
- import { DOMAIN_MODEL_JSON, emitDomainModelJson, resolveShapes, } from "../../../toolkit/derivers/shapes.js";
33
- import { DOMAIN_DTS, emitWorkerBindings, emitWorkerBindingsRuntime, overlayDerivedWorkerIo, WORKER_BINDINGS_DTS, WORKER_BINDINGS_TS, } from "../../../toolkit/derivers/worker-io.js";
34
- import { emitMessageBindings, emitMessageBindingsRuntime, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "../../../toolkit/derivers/messages.js";
35
- import { emitMeta, META_TS } from "../../../toolkit/derivers/meta.js";
36
26
  const DEFAULT_MIGRATIONS_DIR = "db/migrations";
37
27
  /** JSON-safe a value: BigInt → number (when lossless) or string, recursing objects/arrays. SQLite
38
28
  * reports `lastInsertRowid` (and INTEGER columns) as BigInt, which `JSON.stringify` cannot encode. */
@@ -242,6 +232,17 @@ export async function runDataOp(host, root, manifestPath, req) {
242
232
  // debounced keystroke has no DB side-effect and pays only for `text` + `shapeDiagnostics`.
243
233
  const { default: def, sources } = listSources(manifest);
244
234
  const persist = req.write !== false;
235
+ // The derivers are the only toolkit modules `urban data` ever needs, and only this op runs
236
+ // them — load them here, not at module scope, so every other op (the hot path of the data
237
+ // gateway, nano-ide#592) never pays for the `src/toolkit/` tree. Imported directly from the
238
+ // deriver files (not the `toolkit/` barrel, which pulls the Node fs IO) so core stays free
239
+ // of host-specific IO.
240
+ const { GENERATED_DIR } = await import("../../../toolkit/artifact.js");
241
+ const { DOMAIN_BINDINGS, emitDomainBindings, emitDomainModel, registryFromManifest, } = await import("../../../toolkit/derivers/domain.js");
242
+ const { DOMAIN_MODEL_JSON, emitDomainModelJson, resolveShapes } = await import("../../../toolkit/derivers/shapes.js");
243
+ const { DOMAIN_DTS, emitWorkerBindings, emitWorkerBindingsRuntime, overlayDerivedWorkerIo, WORKER_BINDINGS_DTS, WORKER_BINDINGS_TS, } = await import("../../../toolkit/derivers/worker-io.js");
244
+ const { emitMessageBindings, emitMessageBindingsRuntime, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } = await import("../../../toolkit/derivers/messages.js");
245
+ const { emitMeta, META_TS } = await import("../../../toolkit/derivers/meta.js");
245
246
  // Introspect each sqlite source's live schema. When persisting, migrate first (the domain
246
247
  // model is derived from `gw.schema()`, so a regen that raced ahead of `migrate` on a fresh DB
247
248
  // would emit an empty `Domain`); the migration runs on the shared `_urban_migrations` ledger.
@@ -2,7 +2,6 @@ import type { RuntimeContext } from "../context.ts";
2
2
  import type { HostContext, SqliteDb } from "../host.ts";
3
3
  import type { DomainType } from "../manifest.ts";
4
4
  import { Table, type DataSource as GatewayDataSource, type InsertObserver } from "./gateway.ts";
5
- import { isAbsolutePath } from "../../../toolkit/artifact.ts";
6
5
  export declare function sqlitePathFromUrl(url: string): string;
7
6
  /** True when `p` is already an absolute path on any host `urban data` targets. `urban data` runs
8
7
  * on a Node host that may be Windows, so absolute-path detection can't assume POSIX: this matches
@@ -10,10 +9,12 @@ export declare function sqlitePathFromUrl(url: string): string;
10
9
  * that starts with a single backslash ("\data\app.db") and a Windows UNC path
11
10
  * ("\\\\server\\share"). A single leading backslash covers both the drive-root and UNC cases,
12
11
  * matching Node's `path.win32.isAbsolute`. Used by `resolveAppPath` so a caller-supplied absolute
13
- * path is never incorrectly prefixed with the app root. Defined once in `toolkit/artifact.ts` (the
14
- * shared, dependency-free layer) and re-exported here so the toolkit's gen path-join and this
15
- * runtime resolver apply the identical rule (no gen/runtime drift). */
16
- export { isAbsolutePath };
12
+ * path is never incorrectly prefixed with the app root. Defined here in the runtime (not imported
13
+ * from `toolkit/artifact.ts`) so the `urban data` gateway's static module graph never touches the
14
+ * `src/toolkit/` tree (nano-ide#592); `toolkit/artifact.ts` re-exports this definition so the
15
+ * toolkit's gen path-join and this runtime resolver apply the identical rule (no gen/runtime
16
+ * drift). */
17
+ export declare function isAbsolutePath(p: string): boolean;
17
18
  /** Resolve `p` against the app `root`: an absolute `p` (see `isAbsolutePath`) is returned as-is;
18
19
  * a relative `p` is joined onto `root`. Trims a trailing separator of either kind off `root` so
19
20
  * we never emit a doubled separator. The join separator matches the root's own style so we never
@@ -4,7 +4,6 @@
4
4
  // runtime side of ADR 0024 (datasource) + ADR 0040 (domain model), scoped to what an app needs.
5
5
  import { makeGateway, } from "./gateway.js";
6
6
  import { currentJobContext } from "../execContext.js";
7
- import { isAbsolutePath } from "../../../toolkit/artifact.js";
8
7
  import { SQL_IDENT, isReservedObjectName } from "../read-model.js";
9
8
  export function sqlitePathFromUrl(url) {
10
9
  // Accept "file:./x.db", "file:x.db", "sqlite:./x.db" or a bare path.
@@ -16,10 +15,14 @@ export function sqlitePathFromUrl(url) {
16
15
  * that starts with a single backslash ("\data\app.db") and a Windows UNC path
17
16
  * ("\\\\server\\share"). A single leading backslash covers both the drive-root and UNC cases,
18
17
  * matching Node's `path.win32.isAbsolute`. Used by `resolveAppPath` so a caller-supplied absolute
19
- * path is never incorrectly prefixed with the app root. Defined once in `toolkit/artifact.ts` (the
20
- * shared, dependency-free layer) and re-exported here so the toolkit's gen path-join and this
21
- * runtime resolver apply the identical rule (no gen/runtime drift). */
22
- export { isAbsolutePath };
18
+ * path is never incorrectly prefixed with the app root. Defined here in the runtime (not imported
19
+ * from `toolkit/artifact.ts`) so the `urban data` gateway's static module graph never touches the
20
+ * `src/toolkit/` tree (nano-ide#592); `toolkit/artifact.ts` re-exports this definition so the
21
+ * toolkit's gen path-join and this runtime resolver apply the identical rule (no gen/runtime
22
+ * drift). */
23
+ export function isAbsolutePath(p) {
24
+ return /^(\/|\\|[A-Za-z]:[/\\])/.test(p);
25
+ }
23
26
  /** Resolve `p` against the app `root`: an absolute `p` (see `isAbsolutePath`) is returned as-is;
24
27
  * a relative `p` is joined onto `root`. Trims a trailing separator of either kind off `root` so
25
28
  * we never emit a doubled separator. The join separator matches the root's own style so we never
@@ -29,7 +29,9 @@ export declare const RUNTIME_MATERIALIZED_ARTIFACTS: readonly string[];
29
29
  /** Stable ordering so artifact lists compare deterministically. */
30
30
  export declare function sortArtifacts(a: DerivedArtifact[]): DerivedArtifact[];
31
31
  /** True when `p` is an absolute path — POSIX root (`/`), a drive-letter root (`C:\` or `C:/`), or a
32
- * Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule,
33
- * shared by the toolkit's path join (gen.ts) and the runtime's resolveAppPath so both agree on
34
- * whether a manifest path is app-root-relative or absolute (no gen/runtime drift). */
35
- export declare function isAbsolutePath(p: string): boolean;
32
+ * Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule is
33
+ * the runtime's `resolveAppPath` side (`runtime/core/modules/datasource.ts`); it is re-exported
34
+ * here so the toolkit's path join (gen.ts) and the runtime resolver agree on whether a manifest
35
+ * path is app-root-relative or absolute (no gen/runtime drift) without the runtime's data gateway
36
+ * statically importing the toolkit tree (nano-ide#592). */
37
+ export { isAbsolutePath } from "../runtime/core/modules/datasource.ts";
@@ -28,9 +28,9 @@ export function sortArtifacts(a) {
28
28
  return [...a].sort((x, y) => (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
29
29
  }
30
30
  /** True when `p` is an absolute path — POSIX root (`/`), a drive-letter root (`C:\` or `C:/`), or a
31
- * Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule,
32
- * shared by the toolkit's path join (gen.ts) and the runtime's resolveAppPath so both agree on
33
- * whether a manifest path is app-root-relative or absolute (no gen/runtime drift). */
34
- export function isAbsolutePath(p) {
35
- return /^(\/|\\|[A-Za-z]:[/\\])/.test(p);
36
- }
31
+ * Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule is
32
+ * the runtime's `resolveAppPath` side (`runtime/core/modules/datasource.ts`); it is re-exported
33
+ * here so the toolkit's path join (gen.ts) and the runtime resolver agree on whether a manifest
34
+ * path is app-root-relative or absolute (no gen/runtime drift) without the runtime's data gateway
35
+ * statically importing the toolkit tree (nano-ide#592). */
36
+ export { isAbsolutePath } from "../runtime/core/modules/datasource.js";
@@ -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[];
@@ -19,7 +19,8 @@ function join(root, rel) {
19
19
  // pass either style without gen/runtime drift over where a file resolves.
20
20
  const norm = (s) => s.replace(/\\/g, "/");
21
21
  // An absolute `rel` resolves to itself — never prefixed with `root` — mirroring the runtime's
22
- // resolveAppPath (isAbsolutePath is the shared SoT in artifact.ts). Without this an absolute
22
+ // resolveAppPath (isAbsolutePath is the shared SoT, defined in runtime's datasource.ts and
23
+ // re-exported from artifact.ts). Without this an absolute
23
24
  // manifest path (e.g. "/abs/openapi.json") would be stripped to root-relative and gen would
24
25
  // read/derive a different file than the runtime resolves. Trailing edge separators are still
25
26
  // trimmed for stable, comparable artifact keys.
@@ -256,7 +257,7 @@ export async function runGen(opts) {
256
257
  swept.push(rel);
257
258
  }
258
259
  }
259
- 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 };
260
261
  }
261
262
  export { dirOf, join as joinPath };
262
263
  /** Derive code-first models in-memory (no writes) — backs `urban derive --stdout`, the
@@ -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
- const workers = await scaffoldWorkers(scaffoldOpts);
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),
@@ -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. */
@@ -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), 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.
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), and not a duplicate of one already
69
- * planned. `declaredTypeIds` are the manifest `types` ids — an envelope `in`/`out` is typed only
70
- * when it names one (matching the worker-io deriver). `externalTaskTypes` are the manifest's
71
- * declared external task types, which the app owns the model for but deliberately does not host.
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 {
@@ -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 models = await readModels(root, io, manifest);
41
+ const diskModels = await readModels(root, io, manifest);
42
42
  const declaredTypeIds = Object.keys(manifest.types ?? {});
43
- const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds, manifest.externalTaskTypes ?? []);
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.1",
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.0",
109
+ "@nanobpm/workflow": "^0.15.1",
110
110
  "bpmn-auto-layout": "^2.0.0-alpha.2",
111
- "create-urban-app": "^0.15.2",
111
+ "create-urban-app": "^0.15.3",
112
112
  "yaml": "^2.9.0"
113
113
  },
114
114
  "devDependencies": {