@nanobpm/urban 0.38.0 → 0.39.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 +6 -0
- package/dist/toolkit/artifact.d.ts +11 -0
- package/dist/toolkit/artifact.js +18 -0
- package/dist/toolkit/gen.d.ts +2 -0
- package/dist/toolkit/gen.js +24 -2
- package/package.json +1 -1
- package/src/cli.ts +6 -0
- package/src/toolkit/artifact.ts +19 -0
- package/src/toolkit/gen.test.ts +53 -0
- package/src/toolkit/gen.ts +27 -2
package/dist/cli.js
CHANGED
|
@@ -162,6 +162,12 @@ async function cmdGen(f) {
|
|
|
162
162
|
console.log(`✔ generated ${res.artifacts.length} artifact(s):`);
|
|
163
163
|
for (const a of res.artifacts)
|
|
164
164
|
console.log(` • ${a.path}`);
|
|
165
|
+
// Stale generated files removed from nano-generated/ (renamed/removed artifacts from a prior gen).
|
|
166
|
+
if (res.swept.length > 0) {
|
|
167
|
+
console.log(`✔ removed ${res.swept.length} stale generated file(s):`);
|
|
168
|
+
for (const p of res.swept)
|
|
169
|
+
console.log(` - ${p}`);
|
|
170
|
+
}
|
|
165
171
|
// Write-once handler stubs scaffolded as part of generation (workers from the model, operation
|
|
166
172
|
// delegates from the OpenAPI spec). Existing, human-owned stubs are kept verbatim.
|
|
167
173
|
const created = [
|
|
@@ -15,6 +15,17 @@ export interface Deriver<I> {
|
|
|
15
15
|
/** Where all derived output lands — the console's `nano-generated/` dir, so the toolkit is a
|
|
16
16
|
* drop-in for the IDE's own codegen (ADR 0053). One drift domain, gitignored, never committed. */
|
|
17
17
|
export declare const GENERATED_DIR = "nano-generated";
|
|
18
|
+
/**
|
|
19
|
+
* Files that live in `GENERATED_DIR` but are NOT produced by `urban gen`. These are materialized by
|
|
20
|
+
* a different writer — the runtime SDK shims and the `urban data` / console-host dataops path, which
|
|
21
|
+
* derives the typed wrappers from the *live* DB (something `urban gen` can't do without a running
|
|
22
|
+
* datasource). `urban gen`'s stale-file sweep must own only what it emits, so it protects these by
|
|
23
|
+
* name: it never deletes them even though it didn't (re)write them this run. Basenames, relative to
|
|
24
|
+
* `GENERATED_DIR`. Keep in sync with the emitters' filename constants (DOMAIN_BINDINGS,
|
|
25
|
+
* WORKER_BINDINGS_TS, MESSAGE_BINDINGS_TS, DOMAIN_MODEL_JSON) + the `./data-sdk.ts`/`./worker-sdk.ts`
|
|
26
|
+
* shim imports the generated wrappers reference.
|
|
27
|
+
*/
|
|
28
|
+
export declare const RUNTIME_MATERIALIZED_ARTIFACTS: readonly string[];
|
|
18
29
|
/** Stable ordering so artifact lists compare deterministically. */
|
|
19
30
|
export declare function sortArtifacts(a: DerivedArtifact[]): DerivedArtifact[];
|
|
20
31
|
/** True when `p` is an absolute path — POSIX root (`/`), a drive-letter root (`C:\` or `C:/`), or a
|
package/dist/toolkit/artifact.js
CHANGED
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
/** Where all derived output lands — the console's `nano-generated/` dir, so the toolkit is a
|
|
6
6
|
* drop-in for the IDE's own codegen (ADR 0053). One drift domain, gitignored, never committed. */
|
|
7
7
|
export const GENERATED_DIR = "nano-generated";
|
|
8
|
+
/**
|
|
9
|
+
* Files that live in `GENERATED_DIR` but are NOT produced by `urban gen`. These are materialized by
|
|
10
|
+
* a different writer — the runtime SDK shims and the `urban data` / console-host dataops path, which
|
|
11
|
+
* derives the typed wrappers from the *live* DB (something `urban gen` can't do without a running
|
|
12
|
+
* datasource). `urban gen`'s stale-file sweep must own only what it emits, so it protects these by
|
|
13
|
+
* name: it never deletes them even though it didn't (re)write them this run. Basenames, relative to
|
|
14
|
+
* `GENERATED_DIR`. Keep in sync with the emitters' filename constants (DOMAIN_BINDINGS,
|
|
15
|
+
* WORKER_BINDINGS_TS, MESSAGE_BINDINGS_TS, DOMAIN_MODEL_JSON) + the `./data-sdk.ts`/`./worker-sdk.ts`
|
|
16
|
+
* shim imports the generated wrappers reference.
|
|
17
|
+
*/
|
|
18
|
+
export const RUNTIME_MATERIALIZED_ARTIFACTS = [
|
|
19
|
+
"domain.ts",
|
|
20
|
+
"workers.ts",
|
|
21
|
+
"messages.ts",
|
|
22
|
+
"domain.json",
|
|
23
|
+
"data-sdk.ts",
|
|
24
|
+
"worker-sdk.ts",
|
|
25
|
+
];
|
|
8
26
|
/** Stable ordering so artifact lists compare deterministically. */
|
|
9
27
|
export function sortArtifacts(a) {
|
|
10
28
|
return [...a].sort((x, y) => (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
|
package/dist/toolkit/gen.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface GenResult {
|
|
|
36
36
|
artifacts: DerivedArtifact[];
|
|
37
37
|
/** Paths that differ from disk (only populated by `check`). */
|
|
38
38
|
drift: string[];
|
|
39
|
+
/** App-relative paths of stale files removed from `nano-generated/` (only populated in write mode). */
|
|
40
|
+
swept: string[];
|
|
39
41
|
/** True if any code-first workflow failed to import/derive. */
|
|
40
42
|
incomplete: boolean;
|
|
41
43
|
/** Per-file model-derivation errors (empty on success). */
|
package/dist/toolkit/gen.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// models, runs the pure derivers, and either writes the artifacts to disk (`urban gen`) or
|
|
3
3
|
// compares them against what's on disk and reports drift (`urban gen --check`). The derivers stay
|
|
4
4
|
// pure; all IO is confined here behind a tiny FS port so the same code runs on Node and Deno.
|
|
5
|
-
import { isAbsolutePath, sortArtifacts } from "./artifact.js";
|
|
5
|
+
import { GENERATED_DIR, isAbsolutePath, RUNTIME_MATERIALIZED_ARTIFACTS, sortArtifacts } from "./artifact.js";
|
|
6
6
|
import { deriveMigrations } from "./derivers/migrations.js";
|
|
7
7
|
import { deriveDomain } from "./derivers/domain.js";
|
|
8
8
|
import { deriveWorkerBindings } from "./derivers/worker-io.js";
|
|
@@ -138,7 +138,29 @@ export async function runGen(opts) {
|
|
|
138
138
|
await io.remove(join(root, rel));
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
// Stale-artifact sweep of `nano-generated/` (clean break, nano-bpm#: gen owns its output dir).
|
|
142
|
+
// The dir is gitignored and fully codegen-owned, so any file this run did not (re)write is stale
|
|
143
|
+
// from a prior gen — a renamed or removed artifact (e.g. an old `data_sdk.ts`) whose orphan the
|
|
144
|
+
// app might still import. Remove it. The one exception is the runtime-materialized set: the SDK
|
|
145
|
+
// shims + the `urban data`/console dataops wrappers gen does NOT emit but the app needs at runtime
|
|
146
|
+
// (RUNTIME_MATERIALIZED_ARTIFACTS). Protect those by name. Write mode only; skipped when io.remove
|
|
147
|
+
// is absent (the sweep is best-effort, mirroring the `.bpmn` sweep above). NOT on `modelsOnly`
|
|
148
|
+
// (`urban derive`): that path derives only models, so `artifacts` carries no `nano-generated/*` and
|
|
149
|
+
// sweeping would wrongly wipe the type-contract outputs a prior `urban gen` wrote.
|
|
150
|
+
const swept = [];
|
|
151
|
+
if (!opts.check && !opts.modelsOnly && !derived.incomplete && io.remove) {
|
|
152
|
+
const keep = new Set(artifacts.map((a) => a.path).filter((p) => p.startsWith(`${GENERATED_DIR}/`)));
|
|
153
|
+
for (const name of RUNTIME_MATERIALIZED_ARTIFACTS)
|
|
154
|
+
keep.add(`${GENERATED_DIR}/${name}`);
|
|
155
|
+
for (const name of await io.listDir(join(root, GENERATED_DIR))) {
|
|
156
|
+
const rel = `${GENERATED_DIR}/${name}`;
|
|
157
|
+
if (keep.has(rel))
|
|
158
|
+
continue;
|
|
159
|
+
await io.remove(join(root, rel));
|
|
160
|
+
swept.push(rel);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { artifacts, drift, swept: swept.sort(), incomplete: derived.incomplete, modelErrors: derived.errors };
|
|
142
164
|
}
|
|
143
165
|
export { dirOf, join as joinPath };
|
|
144
166
|
/** Derive code-first models in-memory (no writes) — backs `urban derive --stdout`, the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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",
|
package/src/cli.ts
CHANGED
|
@@ -171,6 +171,12 @@ async function cmdGen(f: Flags): Promise<number> {
|
|
|
171
171
|
console.log(`✔ generated ${res.artifacts.length} artifact(s):`);
|
|
172
172
|
for (const a of res.artifacts) console.log(` • ${a.path}`);
|
|
173
173
|
|
|
174
|
+
// Stale generated files removed from nano-generated/ (renamed/removed artifacts from a prior gen).
|
|
175
|
+
if (res.swept.length > 0) {
|
|
176
|
+
console.log(`✔ removed ${res.swept.length} stale generated file(s):`);
|
|
177
|
+
for (const p of res.swept) console.log(` - ${p}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
174
180
|
// Write-once handler stubs scaffolded as part of generation (workers from the model, operation
|
|
175
181
|
// delegates from the OpenAPI spec). Existing, human-owned stubs are kept verbatim.
|
|
176
182
|
const created = [
|
package/src/toolkit/artifact.ts
CHANGED
|
@@ -23,6 +23,25 @@ export interface Deriver<I> {
|
|
|
23
23
|
* drop-in for the IDE's own codegen (ADR 0053). One drift domain, gitignored, never committed. */
|
|
24
24
|
export const GENERATED_DIR = "nano-generated";
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Files that live in `GENERATED_DIR` but are NOT produced by `urban gen`. These are materialized by
|
|
28
|
+
* a different writer — the runtime SDK shims and the `urban data` / console-host dataops path, which
|
|
29
|
+
* derives the typed wrappers from the *live* DB (something `urban gen` can't do without a running
|
|
30
|
+
* datasource). `urban gen`'s stale-file sweep must own only what it emits, so it protects these by
|
|
31
|
+
* name: it never deletes them even though it didn't (re)write them this run. Basenames, relative to
|
|
32
|
+
* `GENERATED_DIR`. Keep in sync with the emitters' filename constants (DOMAIN_BINDINGS,
|
|
33
|
+
* WORKER_BINDINGS_TS, MESSAGE_BINDINGS_TS, DOMAIN_MODEL_JSON) + the `./data-sdk.ts`/`./worker-sdk.ts`
|
|
34
|
+
* shim imports the generated wrappers reference.
|
|
35
|
+
*/
|
|
36
|
+
export const RUNTIME_MATERIALIZED_ARTIFACTS: readonly string[] = [
|
|
37
|
+
"domain.ts",
|
|
38
|
+
"workers.ts",
|
|
39
|
+
"messages.ts",
|
|
40
|
+
"domain.json",
|
|
41
|
+
"data-sdk.ts",
|
|
42
|
+
"worker-sdk.ts",
|
|
43
|
+
];
|
|
44
|
+
|
|
26
45
|
/** Stable ordering so artifact lists compare deterministically. */
|
|
27
46
|
export function sortArtifacts(a: DerivedArtifact[]): DerivedArtifact[] {
|
|
28
47
|
return [...a].sort((x, y) => (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
|
package/src/toolkit/gen.test.ts
CHANGED
|
@@ -103,6 +103,59 @@ test("collectArtifacts touches no writes", async () => {
|
|
|
103
103
|
assert.equal(Object.keys(io.files).length, before);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
test("runGen sweeps stale files from nano-generated/ (write mode)", async () => {
|
|
107
|
+
const io = memIO(fixture());
|
|
108
|
+
// A stale artifact left by a prior gen (e.g. a renamed `data_sdk.ts` orphan) that this run does
|
|
109
|
+
// not re-emit. It must be removed so the app can't import a dead generated module.
|
|
110
|
+
io.files["/app/nano-generated/data_sdk.ts"] = "// stale, renamed away";
|
|
111
|
+
io.files["/app/nano-generated/old-worker-io.d.ts"] = "// stale";
|
|
112
|
+
const res = await runGen({ root: "/app", io });
|
|
113
|
+
assert.deepEqual(res.swept, [
|
|
114
|
+
"nano-generated/data_sdk.ts",
|
|
115
|
+
"nano-generated/old-worker-io.d.ts",
|
|
116
|
+
]);
|
|
117
|
+
assert.ok(!("/app/nano-generated/data_sdk.ts" in io.files), "stale file removed");
|
|
118
|
+
assert.ok(!("/app/nano-generated/old-worker-io.d.ts" in io.files), "stale file removed");
|
|
119
|
+
// Current artifacts survive.
|
|
120
|
+
assert.ok("/app/nano-generated/worker-io.d.ts" in io.files, "fresh artifact kept");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("runGen never sweeps runtime-materialized wrappers gen does not emit", async () => {
|
|
124
|
+
const io = memIO(fixture());
|
|
125
|
+
// The SDK shims + `urban data`/console dataops wrappers live in nano-generated/ but are not
|
|
126
|
+
// produced by `urban gen`; the app needs them at runtime, so the sweep must protect them.
|
|
127
|
+
for (const name of ["domain.ts", "workers.ts", "messages.ts", "domain.json", "data-sdk.ts", "worker-sdk.ts"]) {
|
|
128
|
+
io.files[`/app/nano-generated/${name}`] = `// runtime-materialized ${name}`;
|
|
129
|
+
}
|
|
130
|
+
const res = await runGen({ root: "/app", io });
|
|
131
|
+
assert.deepEqual(res.swept, [], "no runtime-materialized file swept");
|
|
132
|
+
for (const name of ["domain.ts", "workers.ts", "messages.ts", "domain.json", "data-sdk.ts", "worker-sdk.ts"]) {
|
|
133
|
+
assert.ok(`/app/nano-generated/${name}` in io.files, `${name} preserved`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("gen --check never sweeps (read-only)", async () => {
|
|
138
|
+
const io = memIO(fixture());
|
|
139
|
+
await runGen({ root: "/app", io });
|
|
140
|
+
io.files["/app/nano-generated/stale.ts"] = "// stale";
|
|
141
|
+
const res = await runGen({ root: "/app", io, check: true });
|
|
142
|
+
assert.deepEqual(res.swept, []);
|
|
143
|
+
assert.ok("/app/nano-generated/stale.ts" in io.files, "check mode leaves stale files untouched");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("modelsOnly (urban derive) never sweeps nano-generated type-contract outputs", async () => {
|
|
147
|
+
const io = memIO(fixture());
|
|
148
|
+
// A prior `urban gen` wrote the type-contract outputs.
|
|
149
|
+
await runGen({ root: "/app", io });
|
|
150
|
+
assert.ok("/app/nano-generated/worker-io.d.ts" in io.files, "precondition: gen wrote outputs");
|
|
151
|
+
// `urban derive` (modelsOnly) derives only models — its artifacts carry no nano-generated/*, so
|
|
152
|
+
// the sweep must be a no-op here; otherwise it would wipe the type contracts above.
|
|
153
|
+
const res = await runGen({ root: "/app", io, modelsOnly: true });
|
|
154
|
+
assert.deepEqual(res.swept, []);
|
|
155
|
+
assert.ok("/app/nano-generated/worker-io.d.ts" in io.files, "derive preserved the type contracts");
|
|
156
|
+
assert.ok("/app/nano-generated/app.schema.sql" in io.files, "derive preserved the migrations");
|
|
157
|
+
});
|
|
158
|
+
|
|
106
159
|
// --- code-first model derivation (urban gen / urban derive) ---
|
|
107
160
|
|
|
108
161
|
const GreetIn = envelope("GreetIn", { who: "string" });
|
package/src/toolkit/gen.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// pure; all IO is confined here behind a tiny FS port so the same code runs on Node and Deno.
|
|
5
5
|
|
|
6
6
|
import type { DerivedArtifact } from "./artifact.ts";
|
|
7
|
-
import { isAbsolutePath, sortArtifacts } from "./artifact.ts";
|
|
7
|
+
import { GENERATED_DIR, isAbsolutePath, RUNTIME_MATERIALIZED_ARTIFACTS, sortArtifacts } from "./artifact.ts";
|
|
8
8
|
import { deriveMigrations, type ToolkitManifest } from "./derivers/migrations.ts";
|
|
9
9
|
import { deriveDomain } from "./derivers/domain.ts";
|
|
10
10
|
import { deriveWorkerBindings, type ModelSource } from "./derivers/worker-io.ts";
|
|
@@ -51,6 +51,8 @@ export interface GenResult {
|
|
|
51
51
|
artifacts: DerivedArtifact[];
|
|
52
52
|
/** Paths that differ from disk (only populated by `check`). */
|
|
53
53
|
drift: string[];
|
|
54
|
+
/** App-relative paths of stale files removed from `nano-generated/` (only populated in write mode). */
|
|
55
|
+
swept: string[];
|
|
54
56
|
/** True if any code-first workflow failed to import/derive. */
|
|
55
57
|
incomplete: boolean;
|
|
56
58
|
/** Per-file model-derivation errors (empty on success). */
|
|
@@ -199,7 +201,30 @@ export async function runGen(opts: GenOptions & { check?: boolean }): Promise<Ge
|
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
// Stale-artifact sweep of `nano-generated/` (clean break, nano-bpm#: gen owns its output dir).
|
|
205
|
+
// The dir is gitignored and fully codegen-owned, so any file this run did not (re)write is stale
|
|
206
|
+
// from a prior gen — a renamed or removed artifact (e.g. an old `data_sdk.ts`) whose orphan the
|
|
207
|
+
// app might still import. Remove it. The one exception is the runtime-materialized set: the SDK
|
|
208
|
+
// shims + the `urban data`/console dataops wrappers gen does NOT emit but the app needs at runtime
|
|
209
|
+
// (RUNTIME_MATERIALIZED_ARTIFACTS). Protect those by name. Write mode only; skipped when io.remove
|
|
210
|
+
// is absent (the sweep is best-effort, mirroring the `.bpmn` sweep above). NOT on `modelsOnly`
|
|
211
|
+
// (`urban derive`): that path derives only models, so `artifacts` carries no `nano-generated/*` and
|
|
212
|
+
// sweeping would wrongly wipe the type-contract outputs a prior `urban gen` wrote.
|
|
213
|
+
const swept: string[] = [];
|
|
214
|
+
if (!opts.check && !opts.modelsOnly && !derived.incomplete && io.remove) {
|
|
215
|
+
const keep = new Set(
|
|
216
|
+
artifacts.map((a) => a.path).filter((p) => p.startsWith(`${GENERATED_DIR}/`)),
|
|
217
|
+
);
|
|
218
|
+
for (const name of RUNTIME_MATERIALIZED_ARTIFACTS) keep.add(`${GENERATED_DIR}/${name}`);
|
|
219
|
+
for (const name of await io.listDir(join(root, GENERATED_DIR))) {
|
|
220
|
+
const rel = `${GENERATED_DIR}/${name}`;
|
|
221
|
+
if (keep.has(rel)) continue;
|
|
222
|
+
await io.remove(join(root, rel));
|
|
223
|
+
swept.push(rel);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { artifacts, drift, swept: swept.sort(), incomplete: derived.incomplete, modelErrors: derived.errors };
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
export { dirOf, join as joinPath };
|