@nanobpm/urban 0.17.1 → 0.19.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 +131 -5
- package/dist/runtime/adapters/node.js +61 -1
- package/dist/runtime/connector-worker-sdk.d.ts +39 -0
- package/dist/runtime/connector-worker-sdk.js +40 -0
- package/dist/runtime/core/host.d.ts +30 -0
- package/dist/runtime/core/host.js +26 -1
- package/dist/runtime/core/modules/connectors.d.ts +53 -0
- package/dist/runtime/core/modules/connectors.js +204 -0
- package/dist/runtime/core/modules/datasource.js +20 -5
- package/dist/runtime/core/modules/workers.js +5 -0
- package/dist/runtime/core/runtime.js +7 -0
- package/dist/runtime/core/validate.js +34 -8
- package/dist/runtime/engine/nanosdk.d.ts +7 -0
- package/dist/runtime/engine/nanosdk.js +27 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +5 -0
- package/dist/toolkit/addConnector.d.ts +29 -0
- package/dist/toolkit/addConnector.js +170 -0
- package/dist/toolkit/fsio.js +31 -2
- package/dist/toolkit/gen.d.ts +28 -0
- package/dist/toolkit/gen.js +55 -12
- package/dist/toolkit/index.d.ts +5 -1
- package/dist/toolkit/index.js +4 -1
- package/dist/toolkit/models.d.ts +63 -0
- package/dist/toolkit/models.js +142 -0
- package/package.json +2 -2
- package/src/cli.ts +128 -5
- package/src/runtime/adapters/node.ts +64 -1
- package/src/runtime/connector-worker-sdk.test.ts +56 -0
- package/src/runtime/connector-worker-sdk.ts +78 -0
- package/src/runtime/core/host.ts +40 -0
- package/src/runtime/core/modules/connectors.test.ts +206 -0
- package/src/runtime/core/modules/connectors.ts +269 -0
- package/src/runtime/core/modules/datasource.migrations.test.ts +114 -0
- package/src/runtime/core/modules/datasource.ts +24 -5
- package/src/runtime/core/modules/workers.test.ts +13 -0
- package/src/runtime/core/modules/workers.ts +4 -0
- package/src/runtime/core/runtime.ts +6 -0
- package/src/runtime/core/validate.test.ts +24 -0
- package/src/runtime/core/validate.ts +32 -8
- package/src/runtime/engine/nanosdk.test.ts +76 -0
- package/src/runtime/engine/nanosdk.ts +30 -0
- package/src/runtime/index.ts +12 -0
- package/src/toolkit/addConnector.test.ts +161 -0
- package/src/toolkit/addConnector.ts +233 -0
- package/src/toolkit/fsio.test.ts +23 -0
- package/src/toolkit/fsio.ts +22 -2
- package/src/toolkit/gen.test.ts +76 -1
- package/src/toolkit/gen.ts +81 -13
- package/src/toolkit/index.ts +16 -1
- package/src/toolkit/models.test.ts +159 -0
- package/src/toolkit/models.ts +193 -0
package/dist/toolkit/gen.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DerivedArtifact } from "./artifact.ts";
|
|
2
2
|
import { type ModelSource } from "./derivers/worker-io.ts";
|
|
3
|
+
import { type DerivedModels, type ModelError } from "./models.ts";
|
|
3
4
|
/** Minimal filesystem port. Node/Deno impls live in `fsio.ts`. */
|
|
4
5
|
export interface GenIO {
|
|
5
6
|
readText(path: string): Promise<string>;
|
|
@@ -7,19 +8,43 @@ export interface GenIO {
|
|
|
7
8
|
/** File names (not paths) in a directory; empty if it does not exist. */
|
|
8
9
|
listDir(path: string): Promise<string[]>;
|
|
9
10
|
exists(path: string): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Import an app module and return its exports. Optional: only present when the caller runs on
|
|
13
|
+
* a runtime that can load the app's TypeScript (Node with type-stripping, or Deno). Code-first
|
|
14
|
+
* model derivation (`workflows/*.ts` → BPMN) needs it; when absent, gen still derives everything
|
|
15
|
+
* else (migrations, domain, worker I/O from authored/derived models).
|
|
16
|
+
*/
|
|
17
|
+
importModule?(path: string): Promise<Record<string, unknown>>;
|
|
18
|
+
/** Delete a file. Optional: enables the provenance-scoped stale-model sweep (skipped when absent). */
|
|
19
|
+
remove?(path: string): Promise<void>;
|
|
10
20
|
}
|
|
11
21
|
export interface GenOptions {
|
|
12
22
|
root: string;
|
|
13
23
|
io: GenIO;
|
|
14
24
|
manifestFile?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to emit (write) derived `.bpmn` models. Default true. When false, code-first models
|
|
27
|
+
* are still derived *in-memory* to feed the type-contract derivers (worker I/O, meta, messages)
|
|
28
|
+
* — they are just not written to disk or swept. This is the `urban gen --no-models` path, so
|
|
29
|
+
* the console's type-contract regen never mutates `processes/*.bpmn`.
|
|
30
|
+
*/
|
|
31
|
+
emitModels?: boolean;
|
|
32
|
+
/** Derive ONLY the models (skip migrations/domain/worker-I/O). The `urban derive` path. */
|
|
33
|
+
modelsOnly?: boolean;
|
|
15
34
|
}
|
|
16
35
|
export interface GenResult {
|
|
17
36
|
artifacts: DerivedArtifact[];
|
|
18
37
|
/** Paths that differ from disk (only populated by `check`). */
|
|
19
38
|
drift: string[];
|
|
39
|
+
/** True if any code-first workflow failed to import/derive. */
|
|
40
|
+
incomplete: boolean;
|
|
41
|
+
/** Per-file model-derivation errors (empty on success). */
|
|
42
|
+
modelErrors: ModelError[];
|
|
20
43
|
}
|
|
21
44
|
declare function join(root: string, rel: string): string;
|
|
22
45
|
declare function dirOf(p: string): string;
|
|
46
|
+
/** Resolve a `dir/*.ext` (or literal) manifest pattern to file paths relative to root. */
|
|
47
|
+
export declare function expandPattern(root: string, io: GenIO, pattern: string): Promise<string[]>;
|
|
23
48
|
/** Read + resolve the app's process models from the manifest's `models.processes` patterns. */
|
|
24
49
|
export declare function readModels(root: string, io: GenIO, manifest: {
|
|
25
50
|
models?: {
|
|
@@ -33,3 +58,6 @@ export declare function runGen(opts: GenOptions & {
|
|
|
33
58
|
check?: boolean;
|
|
34
59
|
}): Promise<GenResult>;
|
|
35
60
|
export { dirOf, join as joinPath };
|
|
61
|
+
/** Derive code-first models in-memory (no writes) — backs `urban derive --stdout`, the
|
|
62
|
+
* non-mutating preview the console's read-only model viewer uses. */
|
|
63
|
+
export declare function previewModels(opts: GenOptions): Promise<DerivedModels>;
|
package/dist/toolkit/gen.js
CHANGED
|
@@ -8,6 +8,7 @@ import { deriveDomain } from "./derivers/domain.js";
|
|
|
8
8
|
import { deriveWorkerBindings } from "./derivers/worker-io.js";
|
|
9
9
|
import { deriveMeta } from "./derivers/meta.js";
|
|
10
10
|
import { deriveMessageBindings } from "./derivers/messages.js";
|
|
11
|
+
import { deriveModels, MODEL_PROVENANCE } from "./models.js";
|
|
11
12
|
function join(root, rel) {
|
|
12
13
|
// Trim either separator so callers may pass Windows-style paths; GenIO
|
|
13
14
|
// implementations accept forward slashes on all platforms.
|
|
@@ -18,7 +19,7 @@ function dirOf(p) {
|
|
|
18
19
|
return i <= 0 ? "." : p.slice(0, i);
|
|
19
20
|
}
|
|
20
21
|
/** Resolve a `dir/*.ext` (or literal) manifest pattern to file paths relative to root. */
|
|
21
|
-
async function expandPattern(root, io, pattern) {
|
|
22
|
+
export async function expandPattern(root, io, pattern) {
|
|
22
23
|
const star = pattern.indexOf("*");
|
|
23
24
|
if (star === -1) {
|
|
24
25
|
return (await io.exists(join(root, pattern))) ? [pattern] : [];
|
|
@@ -46,29 +47,47 @@ export async function readModels(root, io, manifest) {
|
|
|
46
47
|
}
|
|
47
48
|
/** Collect all artifacts a run would produce, without touching disk beyond reads. */
|
|
48
49
|
export async function collectArtifacts(opts) {
|
|
50
|
+
return (await collectAll(opts)).artifacts;
|
|
51
|
+
}
|
|
52
|
+
/** Internal: collect artifacts AND the model-derivation details (needed by `runGen` for the
|
|
53
|
+
* incomplete signal + the provenance-scoped stale sweep). */
|
|
54
|
+
async function collectAll(opts) {
|
|
49
55
|
const { root, io } = opts;
|
|
56
|
+
const emitModels = opts.emitModels ?? true;
|
|
50
57
|
const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
|
|
51
58
|
const manifest = JSON.parse(await io.readText(manifestPath));
|
|
52
59
|
const artifacts = [];
|
|
53
60
|
// 1. types → migrations + domain row types (DomainTables spine + DomainTypes registry)
|
|
54
|
-
if (manifest.types && Object.keys(manifest.types).length > 0) {
|
|
61
|
+
if (!opts.modelsOnly && manifest.types && Object.keys(manifest.types).length > 0) {
|
|
55
62
|
artifacts.push(...deriveMigrations(manifest));
|
|
56
63
|
artifacts.push(...deriveDomain(manifest));
|
|
57
64
|
}
|
|
58
|
-
// 2. models →
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
artifacts.push(...
|
|
64
|
-
|
|
65
|
+
// 2. code-first models → derive BPMN from `workflows/*.ts` (executes the app's TS). The derived
|
|
66
|
+
// `.bpmn` are emitted only when `emitModels` (default), but always fed to the type-contract
|
|
67
|
+
// derivers below so worker I/O works for a code-first app even on a `--no-models` run.
|
|
68
|
+
const derived = await deriveModels(root, io, manifest);
|
|
69
|
+
if (emitModels)
|
|
70
|
+
artifacts.push(...derived.artifacts);
|
|
71
|
+
// 3. models (authored on-disk + derived) → worker I/O index + meta accessor + message map.
|
|
72
|
+
// Drop any on-disk model whose path a derived model already covers, to avoid double-counting.
|
|
73
|
+
if (!opts.modelsOnly) {
|
|
74
|
+
const derivedPaths = new Set(derived.artifacts.map((a) => a.path));
|
|
75
|
+
const diskModels = (await readModels(root, io, manifest)).filter((m) => !derivedPaths.has(m.path));
|
|
76
|
+
const models = [...diskModels, ...derived.models];
|
|
77
|
+
if (models.length > 0) {
|
|
78
|
+
const declaredTypeIds = Object.keys(manifest.types ?? {});
|
|
79
|
+
artifacts.push(...deriveWorkerBindings(models, declaredTypeIds));
|
|
80
|
+
artifacts.push(...deriveMeta(models));
|
|
81
|
+
artifacts.push(...deriveMessageBindings(models, declaredTypeIds));
|
|
82
|
+
}
|
|
65
83
|
}
|
|
66
|
-
return sortArtifacts(artifacts);
|
|
84
|
+
return { artifacts: sortArtifacts(artifacts), derived };
|
|
67
85
|
}
|
|
68
86
|
/** Run the derivers and write artifacts (or, with `check`, report drift without writing). */
|
|
69
87
|
export async function runGen(opts) {
|
|
70
|
-
const artifacts = await
|
|
88
|
+
const { artifacts, derived } = await collectAll(opts);
|
|
71
89
|
const { root, io } = opts;
|
|
90
|
+
const emitModels = opts.emitModels ?? true;
|
|
72
91
|
const drift = [];
|
|
73
92
|
for (const a of artifacts) {
|
|
74
93
|
const abs = join(root, a.path);
|
|
@@ -81,6 +100,30 @@ export async function runGen(opts) {
|
|
|
81
100
|
await io.writeText(abs, a.content);
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
|
-
|
|
103
|
+
// Provenance-scoped stale sweep: delete derived `.bpmn` for flows that no longer exist. Only
|
|
104
|
+
// in write mode, only when we emitted models, only on a complete derivation (never delete when
|
|
105
|
+
// a workflow failed to load), and only files bearing our marker (authored models are untouched).
|
|
106
|
+
if (!opts.check && emitModels && derived.attempted && !derived.incomplete && io.remove) {
|
|
107
|
+
const keep = new Set(derived.artifacts.map((a) => a.path));
|
|
108
|
+
for (const name of await io.listDir(join(root, derived.outDir))) {
|
|
109
|
+
if (!name.endsWith(".bpmn"))
|
|
110
|
+
continue;
|
|
111
|
+
const rel = `${derived.outDir}/${name}`;
|
|
112
|
+
if (keep.has(rel))
|
|
113
|
+
continue;
|
|
114
|
+
const content = await io.readText(join(root, rel));
|
|
115
|
+
if (content.includes(MODEL_PROVENANCE))
|
|
116
|
+
await io.remove(join(root, rel));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { artifacts, drift, incomplete: derived.incomplete, modelErrors: derived.errors };
|
|
85
120
|
}
|
|
86
121
|
export { dirOf, join as joinPath };
|
|
122
|
+
/** Derive code-first models in-memory (no writes) — backs `urban derive --stdout`, the
|
|
123
|
+
* non-mutating preview the console's read-only model viewer uses. */
|
|
124
|
+
export async function previewModels(opts) {
|
|
125
|
+
const { root, io } = opts;
|
|
126
|
+
const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
|
|
127
|
+
const manifest = JSON.parse(await io.readText(manifestPath));
|
|
128
|
+
return deriveModels(root, io, manifest);
|
|
129
|
+
}
|
package/dist/toolkit/index.d.ts
CHANGED
|
@@ -20,10 +20,14 @@ export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import(
|
|
|
20
20
|
models: import("./derivers/worker-io.ts").ModelSource[];
|
|
21
21
|
declaredTypeIds: Iterable<string>;
|
|
22
22
|
}>];
|
|
23
|
-
export { collectArtifacts, runGen, joinPath, readModels } from "./gen.ts";
|
|
23
|
+
export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.ts";
|
|
24
24
|
export type { GenIO, GenOptions, GenResult } from "./gen.ts";
|
|
25
25
|
export { createNodeGenIO } from "./fsio.ts";
|
|
26
|
+
export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.ts";
|
|
27
|
+
export type { DerivedModels, DerivedModelInfo, ModelError, ModelsManifest } from "./models.ts";
|
|
26
28
|
export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffold/workers.ts";
|
|
27
29
|
export type { ScaffoldWorker, SkippedWorker, SkipReason, StubManifestEntry, WorkerScaffoldPlan, WorkerStubPlan, } from "./scaffold/workers.ts";
|
|
28
30
|
export { scaffoldWorkers } from "./scaffold.ts";
|
|
29
31
|
export type { ScaffoldOptions, ScaffoldRun, StubOutcome, StubStatus } from "./scaffold.ts";
|
|
32
|
+
export { addConnector } from "./addConnector.ts";
|
|
33
|
+
export type { AddConnectorOptions, AddConnectorResult } from "./addConnector.ts";
|
package/dist/toolkit/index.js
CHANGED
|
@@ -18,8 +18,11 @@ import { metaDeriver } from "./derivers/meta.js";
|
|
|
18
18
|
import { messagesDeriver } from "./derivers/messages.js";
|
|
19
19
|
export const DERIVERS = [migrationsDeriver, domainDeriver, workerIoDeriver, metaDeriver, messagesDeriver];
|
|
20
20
|
// Gen orchestrator + IO
|
|
21
|
-
export { collectArtifacts, runGen, joinPath, readModels } from "./gen.js";
|
|
21
|
+
export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.js";
|
|
22
22
|
export { createNodeGenIO } from "./fsio.js";
|
|
23
|
+
// Code-first model derivation (workflows/*.ts → BPMN)
|
|
24
|
+
export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.js";
|
|
23
25
|
// Worker-stub scaffolder (ADR 0056): write-once handler stubs from the model.
|
|
24
26
|
export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffold/workers.js";
|
|
25
27
|
export { scaffoldWorkers } from "./scaffold.js";
|
|
28
|
+
export { addConnector } from "./addConnector.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Workflow } from "@nanobpm/workflow";
|
|
2
|
+
import type { DerivedArtifact } from "./artifact.ts";
|
|
3
|
+
import type { GenIO } from "./gen.ts";
|
|
4
|
+
import type { ModelSource } from "./derivers/worker-io.ts";
|
|
5
|
+
/** Default source glob for code-first flows (matches the code-first scaffold layout). */
|
|
6
|
+
export declare const DEFAULT_WORKFLOW_PATTERNS: string[];
|
|
7
|
+
/** Default output dir for derived models (matches the model-first scaffold, so a derived
|
|
8
|
+
* model is ejectable to model-first and picked up by `models.processes: ["processes/*.bpmn"]`). */
|
|
9
|
+
export declare const PROCESSES_DIR = "processes";
|
|
10
|
+
/** Marker stamped into every derived `.bpmn` (an XML comment) so re-gen overwrites and the
|
|
11
|
+
* stale sweep deletes ONLY derived output — never an authored, human-owned model. */
|
|
12
|
+
export declare const MODEL_PROVENANCE = "Generated by @nanobpm/urban from workflows/*.ts \u2014 do not edit; edit the flow and re-run `urban gen`.";
|
|
13
|
+
/** Minimal shape a code-first manifest declares for model derivation. */
|
|
14
|
+
export interface ModelsManifest {
|
|
15
|
+
models?: {
|
|
16
|
+
processes?: string[];
|
|
17
|
+
workflows?: string[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** A `defineFlow` export: has a string `id` and an `imperative`/`declarative` `kind`. */
|
|
21
|
+
export declare function isWorkflow(v: unknown): v is Workflow;
|
|
22
|
+
/** Sanitize a flow id into a safe `.bpmn` filename (mirrors the host generator). */
|
|
23
|
+
export declare function bpmnFilename(id: string): string;
|
|
24
|
+
/** The directory derived models are written to: the dir of the first `models.processes`
|
|
25
|
+
* glob if the manifest declares one, else the `processes/` default. */
|
|
26
|
+
export declare function processesOutDir(manifest: ModelsManifest): string;
|
|
27
|
+
export interface ModelError {
|
|
28
|
+
path: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DerivedModels {
|
|
32
|
+
/** `processes/<id>.bpmn` artifacts (path + provenance-stamped content). */
|
|
33
|
+
artifacts: DerivedArtifact[];
|
|
34
|
+
/** The same models, in-memory, for feeding the type-contract derivers (worker I/O, meta, messages). */
|
|
35
|
+
models: ModelSource[];
|
|
36
|
+
/** Derived flow ids (used for the provenance-scoped stale sweep). */
|
|
37
|
+
ids: string[];
|
|
38
|
+
/** Per-flow `{ id, kind, xml }` for the non-writing preview (`urban derive --stdout`). */
|
|
39
|
+
list: DerivedModelInfo[];
|
|
40
|
+
/** The output directory the artifacts were written into. */
|
|
41
|
+
outDir: string;
|
|
42
|
+
/** True once we attempted derivation (loader present AND at least one workflow file matched). */
|
|
43
|
+
attempted: boolean;
|
|
44
|
+
/** True if any workflow module failed to import/derive — suppresses the destructive sweep. */
|
|
45
|
+
incomplete: boolean;
|
|
46
|
+
/** Per-file derivation errors (empty on full success). */
|
|
47
|
+
errors: ModelError[];
|
|
48
|
+
}
|
|
49
|
+
export interface DerivedModelInfo {
|
|
50
|
+
id: string;
|
|
51
|
+
kind: Workflow["kind"];
|
|
52
|
+
xml: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Derive executable BPMN models from an app's code-first `workflows/*.ts`.
|
|
56
|
+
*
|
|
57
|
+
* Returns everything in-memory (artifacts + `ModelSource`s + ids). The caller decides whether
|
|
58
|
+
* to write the `.bpmn` to disk (`urban gen` / `urban derive`) or keep them in memory to feed
|
|
59
|
+
* the type-contract derivers only (`urban gen --no-models`) or preview them (`urban derive
|
|
60
|
+
* --stdout`). Never throws on a bad workflow: a failing module is recorded in `errors`,
|
|
61
|
+
* `incomplete` is set, and the sweep is suppressed so a partial failure can't delete good models.
|
|
62
|
+
*/
|
|
63
|
+
export declare function deriveModels(root: string, io: GenIO, manifest: ModelsManifest): Promise<DerivedModels>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Model derivation for code-first apps — the toolkit's one *executing* edge.
|
|
2
|
+
//
|
|
3
|
+
// Unlike the pure derivers (migrations, domain, worker I/O), which transform strings, this
|
|
4
|
+
// imports the app's `workflows/*.ts`, turns each `defineFlow` into executable BPMN via
|
|
5
|
+
// `@nanobpm/workflow` (`toBpmn` + `bpmn-auto-layout` DI), and writes `processes/<id>.bpmn`.
|
|
6
|
+
// Executing the app's TypeScript needs a runtime module loader, so this is gated on the
|
|
7
|
+
// optional `GenIO.importModule` (present on Node with type-stripping, and on Deno). When the
|
|
8
|
+
// loader is absent — or the app authors its models directly (model-first) — derivation is a
|
|
9
|
+
// no-op and the rest of `urban gen` proceeds on the authored/derived models unchanged.
|
|
10
|
+
import { toDeployableBpmn } from "@nanobpm/workflow";
|
|
11
|
+
import { expandPattern, joinPath } from "./gen.js";
|
|
12
|
+
/** Default source glob for code-first flows (matches the code-first scaffold layout). */
|
|
13
|
+
export const DEFAULT_WORKFLOW_PATTERNS = ["workflows/*.ts"];
|
|
14
|
+
/** Default output dir for derived models (matches the model-first scaffold, so a derived
|
|
15
|
+
* model is ejectable to model-first and picked up by `models.processes: ["processes/*.bpmn"]`). */
|
|
16
|
+
export const PROCESSES_DIR = "processes";
|
|
17
|
+
/** Marker stamped into every derived `.bpmn` (an XML comment) so re-gen overwrites and the
|
|
18
|
+
* stale sweep deletes ONLY derived output — never an authored, human-owned model. */
|
|
19
|
+
export const MODEL_PROVENANCE = "Generated by @nanobpm/urban from workflows/*.ts — do not edit; edit the flow and re-run `urban gen`.";
|
|
20
|
+
/** A `defineFlow` export: has a string `id` and an `imperative`/`declarative` `kind`. */
|
|
21
|
+
export function isWorkflow(v) {
|
|
22
|
+
if (!v || typeof v !== "object")
|
|
23
|
+
return false;
|
|
24
|
+
const o = v;
|
|
25
|
+
return typeof o.id === "string" && (o.kind === "imperative" || o.kind === "declarative");
|
|
26
|
+
}
|
|
27
|
+
/** Sanitize a flow id into a safe `.bpmn` filename (mirrors the host generator). */
|
|
28
|
+
export function bpmnFilename(id) {
|
|
29
|
+
return `${id.replace(/[^A-Za-z0-9._-]/g, "-")}.bpmn`;
|
|
30
|
+
}
|
|
31
|
+
/** The directory derived models are written to: the dir of the first `models.processes`
|
|
32
|
+
* glob if the manifest declares one, else the `processes/` default. */
|
|
33
|
+
export function processesOutDir(manifest) {
|
|
34
|
+
for (const pat of manifest.models?.processes ?? []) {
|
|
35
|
+
const star = pat.indexOf("*");
|
|
36
|
+
const slash = star === -1 ? pat.lastIndexOf("/") : pat.lastIndexOf("/", star);
|
|
37
|
+
if (slash > 0)
|
|
38
|
+
return pat.slice(0, slash);
|
|
39
|
+
}
|
|
40
|
+
return PROCESSES_DIR;
|
|
41
|
+
}
|
|
42
|
+
/** Insert the provenance marker as an XML comment right after the `<?xml … ?>` prolog. */
|
|
43
|
+
function withProvenance(xml) {
|
|
44
|
+
const comment = `<!-- ${MODEL_PROVENANCE} -->`;
|
|
45
|
+
const m = xml.match(/^\uFEFF?\s*<\?xml[^>]*\?>/);
|
|
46
|
+
if (m)
|
|
47
|
+
return `${m[0]}\n${comment}\n${xml.slice(m[0].length).replace(/^\r?\n/, "")}`;
|
|
48
|
+
return `${comment}\n${xml}`;
|
|
49
|
+
}
|
|
50
|
+
function emptyDerived(outDir) {
|
|
51
|
+
return {
|
|
52
|
+
artifacts: [],
|
|
53
|
+
models: [],
|
|
54
|
+
ids: [],
|
|
55
|
+
list: [],
|
|
56
|
+
outDir,
|
|
57
|
+
attempted: false,
|
|
58
|
+
incomplete: false,
|
|
59
|
+
errors: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function messageOf(e) {
|
|
63
|
+
return e instanceof Error ? e.message : String(e);
|
|
64
|
+
}
|
|
65
|
+
/** Append an actionable hint when a `.ts` import fails because the runtime can't strip types. */
|
|
66
|
+
function importHint(rel, e) {
|
|
67
|
+
const base = messageOf(e);
|
|
68
|
+
const code = e?.code ?? "";
|
|
69
|
+
const looksLikeNoTsLoader = rel.endsWith(".ts") &&
|
|
70
|
+
(code === "ERR_UNKNOWN_FILE_EXTENSION" || /Unknown file extension "\.ts"/.test(base));
|
|
71
|
+
if (!looksLikeNoTsLoader)
|
|
72
|
+
return base;
|
|
73
|
+
return (`${base}\n ↳ importing TypeScript workflows needs a runtime that strips types: ` +
|
|
74
|
+
"Node ≥22.18 (default) or `node --experimental-strip-types`, or run under Deno.");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Derive executable BPMN models from an app's code-first `workflows/*.ts`.
|
|
78
|
+
*
|
|
79
|
+
* Returns everything in-memory (artifacts + `ModelSource`s + ids). The caller decides whether
|
|
80
|
+
* to write the `.bpmn` to disk (`urban gen` / `urban derive`) or keep them in memory to feed
|
|
81
|
+
* the type-contract derivers only (`urban gen --no-models`) or preview them (`urban derive
|
|
82
|
+
* --stdout`). Never throws on a bad workflow: a failing module is recorded in `errors`,
|
|
83
|
+
* `incomplete` is set, and the sweep is suppressed so a partial failure can't delete good models.
|
|
84
|
+
*/
|
|
85
|
+
export async function deriveModels(root, io, manifest) {
|
|
86
|
+
const outDir = processesOutDir(manifest);
|
|
87
|
+
const importModule = io.importModule;
|
|
88
|
+
if (!importModule)
|
|
89
|
+
return emptyDerived(outDir);
|
|
90
|
+
const patterns = manifest.models?.workflows ?? DEFAULT_WORKFLOW_PATTERNS;
|
|
91
|
+
const files = [];
|
|
92
|
+
for (const pat of patterns) {
|
|
93
|
+
for (const rel of await expandPattern(root, io, pat))
|
|
94
|
+
files.push(rel);
|
|
95
|
+
}
|
|
96
|
+
if (files.length === 0)
|
|
97
|
+
return emptyDerived(outDir);
|
|
98
|
+
const out = { ...emptyDerived(outDir), attempted: true };
|
|
99
|
+
// id → { source file, workflow ref } — distinguishes a genuine re-export (same
|
|
100
|
+
// object under two export names) from a true collision (two distinct flows, one id).
|
|
101
|
+
const byId = new Map();
|
|
102
|
+
for (const rel of files.sort()) {
|
|
103
|
+
let mod;
|
|
104
|
+
try {
|
|
105
|
+
mod = await importModule(joinPath(root, rel));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
out.incomplete = true;
|
|
109
|
+
out.errors.push({ path: rel, message: importHint(rel, e) });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const value of Object.values(mod)) {
|
|
113
|
+
if (!isWorkflow(value))
|
|
114
|
+
continue;
|
|
115
|
+
const id = value.id;
|
|
116
|
+
const prior = byId.get(id);
|
|
117
|
+
if (prior) {
|
|
118
|
+
if (prior.ref === value && prior.rel === rel)
|
|
119
|
+
continue; // genuine intra-module re-export
|
|
120
|
+
out.incomplete = true;
|
|
121
|
+
const where = prior.rel === rel ? "also defined in the same module" : `also defined in ${prior.rel}`;
|
|
122
|
+
out.errors.push({ path: rel, message: `duplicate flow id "${id}" (${where})` });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
byId.set(id, { rel, ref: value });
|
|
126
|
+
try {
|
|
127
|
+
const semantic = await toDeployableBpmn(value);
|
|
128
|
+
const xml = withProvenance(semantic);
|
|
129
|
+
const path = `${outDir}/${bpmnFilename(id)}`;
|
|
130
|
+
out.artifacts.push({ path, content: xml });
|
|
131
|
+
out.models.push({ path, xml });
|
|
132
|
+
out.list.push({ id, kind: value.kind, xml });
|
|
133
|
+
out.ids.push(id);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
out.incomplete = true;
|
|
137
|
+
out.errors.push({ path: rel, message: `deriving "${id}": ${messageOf(e)}` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@nanobpm/nano-app-schema": "^0.1.0",
|
|
66
66
|
"@nanobpm/nano-sdk": "^1.2.3",
|
|
67
|
-
"@nanobpm/workflow": "^0.
|
|
67
|
+
"@nanobpm/workflow": "^0.8.0",
|
|
68
68
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
69
69
|
"create-urban-app": "^0.5.0"
|
|
70
70
|
},
|
package/src/cli.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// urban new <name> scaffold a new app (delegates to create-urban-app)
|
|
4
4
|
// urban check load + validate the manifest, report issues
|
|
5
|
-
// urban gen derive generated artifacts (migrations, worker I/O)
|
|
5
|
+
// urban gen derive generated artifacts (migrations, worker I/O, code-first models)
|
|
6
|
+
// urban derive derive code-first models only (workflows/*.ts → processes/*.bpmn)
|
|
6
7
|
// urban run materialize + serve the app (deploy, data, workers, surfaces, triggers)
|
|
7
8
|
// urban dev like run, plus watch sources and hot-reload on change
|
|
8
9
|
// urban deploy deploy models only, then exit
|
|
@@ -19,9 +20,10 @@ import {
|
|
|
19
20
|
selectHost,
|
|
20
21
|
} from "./runtime/index.ts";
|
|
21
22
|
import { scaffold, slugify } from "create-urban-app";
|
|
22
|
-
import { createNodeGenIO, runGen, scaffoldWorkers } from "./toolkit/index.ts";
|
|
23
|
+
import { addConnector, createNodeGenIO, previewModels, runGen, scaffoldWorkers } from "./toolkit/index.ts";
|
|
23
24
|
import { pathToFileURL } from "node:url";
|
|
24
25
|
import { readFileSync } from "node:fs";
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
25
27
|
|
|
26
28
|
// Read the package version from package.json (one hop up from both src/cli.ts and the
|
|
27
29
|
// compiled dist/cli.js) so `urban --version` never drifts from the published version.
|
|
@@ -44,6 +46,9 @@ interface Flags {
|
|
|
44
46
|
check: boolean;
|
|
45
47
|
write: boolean;
|
|
46
48
|
deno: boolean;
|
|
49
|
+
models: boolean;
|
|
50
|
+
stdout: boolean;
|
|
51
|
+
install: boolean;
|
|
47
52
|
style?: "model" | "code";
|
|
48
53
|
help: boolean;
|
|
49
54
|
version: boolean;
|
|
@@ -51,7 +56,7 @@ interface Flags {
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function parse(argv: string[]): Flags {
|
|
54
|
-
const f: Flags = { root: ".", manifest: "nano.app.json", check: false, write: false, deno: false, help: false, version: false, _: [] };
|
|
59
|
+
const f: Flags = { root: ".", manifest: "nano.app.json", check: false, write: false, deno: false, models: true, stdout: false, install: true, help: false, version: false, _: [] };
|
|
55
60
|
const need = (i: number, flag: string): string => {
|
|
56
61
|
const v = argv[i];
|
|
57
62
|
if (v === undefined || v.startsWith("-")) throw new Error(`flag ${flag} requires a value`);
|
|
@@ -64,6 +69,11 @@ function parse(argv: string[]): Flags {
|
|
|
64
69
|
else if (a === "--check") f.check = true;
|
|
65
70
|
else if (a === "--write") f.write = true;
|
|
66
71
|
else if (a === "--deno") f.deno = true;
|
|
72
|
+
else if (a === "--no-models") f.models = false;
|
|
73
|
+
else if (a === "--models") f.models = true;
|
|
74
|
+
else if (a === "--stdout") f.stdout = true;
|
|
75
|
+
else if (a === "--no-install") f.install = false;
|
|
76
|
+
else if (a === "--install") f.install = true;
|
|
67
77
|
else if (a === "--code-first") f.style = "code";
|
|
68
78
|
else if (a === "--style") {
|
|
69
79
|
const v = need(++i, a);
|
|
@@ -92,8 +102,12 @@ Usage:
|
|
|
92
102
|
urban new <name> [--root <path>] [--deno] [--style model|code] scaffold a new Urban app
|
|
93
103
|
(--code-first is shorthand for --style code)
|
|
94
104
|
urban check validate the manifest
|
|
95
|
-
urban gen [--check]
|
|
105
|
+
urban gen [--check] [--no-models] derive artifacts (migrations, worker-io, + code-first models)
|
|
106
|
+
(--no-models: skip writing derived .bpmn; type-contracts only)
|
|
107
|
+
urban derive [--check|--stdout] derive code-first models only (workflows/*.ts → processes/*.bpmn)
|
|
108
|
+
(--stdout: print {models,incomplete} JSON without writing)
|
|
96
109
|
urban stubs [--write] scaffold write-once handler stubs from the model
|
|
110
|
+
urban add <pkg> [--no-install] install a connector pack + wire it into nano.app.json
|
|
97
111
|
urban run materialize + serve the app
|
|
98
112
|
urban dev run + watch sources, hot-reload on change
|
|
99
113
|
urban deploy deploy models only, then exit
|
|
@@ -125,7 +139,12 @@ async function cmdCheck(f: Flags): Promise<number> {
|
|
|
125
139
|
|
|
126
140
|
async function cmdGen(f: Flags): Promise<number> {
|
|
127
141
|
const io = createNodeGenIO();
|
|
128
|
-
const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check });
|
|
142
|
+
const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check, emitModels: f.models });
|
|
143
|
+
if (res.incomplete) {
|
|
144
|
+
console.error(`✖ ${res.modelErrors.length} workflow(s) failed to derive:`);
|
|
145
|
+
for (const e of res.modelErrors) console.error(` • ${e.path}: ${e.message}`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
129
148
|
if (f.check) {
|
|
130
149
|
if (res.drift.length === 0) {
|
|
131
150
|
console.log(`✔ generated artifacts are up to date (${res.artifacts.length} checked)`);
|
|
@@ -140,6 +159,55 @@ async function cmdGen(f: Flags): Promise<number> {
|
|
|
140
159
|
return 0;
|
|
141
160
|
}
|
|
142
161
|
|
|
162
|
+
async function cmdDerive(f: Flags): Promise<number> {
|
|
163
|
+
// `urban derive` IS model derivation, so `--no-models` is contradictory. Reject it explicitly
|
|
164
|
+
// rather than silently ignoring it (flags are global, so it's easy to pass by accident).
|
|
165
|
+
if (!f.models) {
|
|
166
|
+
console.error(
|
|
167
|
+
"✖ `urban derive` derives models — `--no-models` is contradictory. Use `urban gen --no-models` for type-contracts only.",
|
|
168
|
+
);
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
171
|
+
const io = createNodeGenIO();
|
|
172
|
+
|
|
173
|
+
// Non-writing preview: emit `{ models: [{ id, kind, xml }], incomplete }` to stdout for the
|
|
174
|
+
// console's read-only model viewer (never touches `processes/`).
|
|
175
|
+
if (f.stdout) {
|
|
176
|
+
const d = await previewModels({ root: f.root, io, manifestFile: f.manifest });
|
|
177
|
+
process.stdout.write(
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
models: d.list.map((m) => ({ id: m.id, kind: m.kind, xml: m.xml })),
|
|
180
|
+
incomplete: d.incomplete,
|
|
181
|
+
}) + "\n",
|
|
182
|
+
);
|
|
183
|
+
if (d.incomplete) {
|
|
184
|
+
for (const e of d.errors) console.error(` • ${e.path}: ${e.message}`);
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Derive + write models only (skip the type-contract derivers); `generate_models` delegates here.
|
|
191
|
+
const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check, modelsOnly: true });
|
|
192
|
+
if (res.incomplete) {
|
|
193
|
+
console.error(`✖ ${res.modelErrors.length} workflow(s) failed to derive:`);
|
|
194
|
+
for (const e of res.modelErrors) console.error(` • ${e.path}: ${e.message}`);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
if (f.check) {
|
|
198
|
+
if (res.drift.length === 0) {
|
|
199
|
+
console.log(`✔ derived models are up to date (${res.artifacts.length} checked)`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
console.error(`✖ ${res.drift.length} derived model(s) are out of date — run \`urban derive\`:`);
|
|
203
|
+
for (const p of res.drift) console.error(` • ${p}`);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
console.log(`✔ derived ${res.artifacts.length} model(s):`);
|
|
207
|
+
for (const a of res.artifacts) console.log(` • ${a.path}`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
143
211
|
async function cmdStubs(f: Flags): Promise<number> {
|
|
144
212
|
const io = createNodeGenIO();
|
|
145
213
|
const run = await scaffoldWorkers({ root: f.root, io, manifestFile: f.manifest, write: f.write });
|
|
@@ -218,6 +286,57 @@ async function cmdNew(f: Flags): Promise<number> {
|
|
|
218
286
|
return 0;
|
|
219
287
|
}
|
|
220
288
|
|
|
289
|
+
async function cmdAdd(f: Flags): Promise<number> {
|
|
290
|
+
const pkg = f._[1];
|
|
291
|
+
if (!pkg) {
|
|
292
|
+
console.error("usage: urban add <pkg> [--no-install]");
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
// Install the connector pack (a subprocess — the toolkit stays runtime-agnostic).
|
|
296
|
+
if (f.install) {
|
|
297
|
+
console.log(`Installing ${pkg} …`);
|
|
298
|
+
const res = spawnSync("npm", ["install", pkg], {
|
|
299
|
+
cwd: f.root,
|
|
300
|
+
stdio: "inherit",
|
|
301
|
+
});
|
|
302
|
+
if (res.error) {
|
|
303
|
+
console.error(`npm install failed: ${res.error.message}`);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
if (res.signal) {
|
|
307
|
+
console.error(`npm install was terminated by signal ${res.signal}`);
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
if (res.status !== 0) {
|
|
311
|
+
console.error(`npm install exited with code ${res.status}`);
|
|
312
|
+
return res.status ?? 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const result = await addConnector({
|
|
318
|
+
root: f.root,
|
|
319
|
+
pkg,
|
|
320
|
+
manifestFile: f.manifest,
|
|
321
|
+
io: createNodeGenIO(),
|
|
322
|
+
});
|
|
323
|
+
const added = result.wired.filter((w) => !w.alreadyPresent);
|
|
324
|
+
const kept = result.wired.filter((w) => w.alreadyPresent);
|
|
325
|
+
console.log(`✔ enabled connector "${result.packId}" from ${pkg}`);
|
|
326
|
+
for (const w of added) console.log(` + worker ${w.taskType}`);
|
|
327
|
+
for (const w of kept) console.log(` = worker ${w.taskType} (already wired)`);
|
|
328
|
+
if (result.connection) console.log(` connection: ${result.connection}`);
|
|
329
|
+
if (result.requiredEnv.length > 0) {
|
|
330
|
+
console.log(`\nSet these environment variables before \`urban run\`:`);
|
|
331
|
+
for (const name of result.requiredEnv) console.log(` ${name}`);
|
|
332
|
+
}
|
|
333
|
+
return 0;
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error(`✖ ${(err as Error).message}`);
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
221
340
|
export async function main(argv: string[]): Promise<number> {
|
|
222
341
|
let f: Flags;
|
|
223
342
|
try {
|
|
@@ -243,8 +362,12 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
243
362
|
return cmdCheck(f);
|
|
244
363
|
case "gen":
|
|
245
364
|
return cmdGen(f);
|
|
365
|
+
case "derive":
|
|
366
|
+
return cmdDerive(f);
|
|
246
367
|
case "stubs":
|
|
247
368
|
return cmdStubs(f);
|
|
369
|
+
case "add":
|
|
370
|
+
return cmdAdd(f);
|
|
248
371
|
case "run":
|
|
249
372
|
return cmdRun(f);
|
|
250
373
|
case "dev":
|