@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 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
@@ -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));
@@ -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). */
@@ -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
- return { artifacts, drift, incomplete: derived.incomplete, modelErrors: derived.errors };
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.38.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 = [
@@ -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));
@@ -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" });
@@ -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
- return { artifacts, drift, incomplete: derived.incomplete, modelErrors: derived.errors };
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 };