@cosmicdrift/kumiko-framework 0.34.2 → 0.36.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"zod": "^4.4.3"
|
|
182
182
|
},
|
|
183
183
|
"devDependencies": {
|
|
184
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
184
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.35.0",
|
|
185
185
|
"@types/uuid": "^11.0.0",
|
|
186
186
|
"bun-types": "^1.3.13",
|
|
187
187
|
"pino-pretty": "^13.1.3"
|
|
@@ -151,4 +151,86 @@ describe("buildAppSchema", () => {
|
|
|
151
151
|
// Feature-namen identisch nach Roundtrip
|
|
152
152
|
expect(parsed.features[0].featureName).toBe("ent");
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
test("FormatSpec-Renderer + FieldCondition-RowActions überleben JSON-Roundtrip unverändert", () => {
|
|
156
|
+
// Pinnt: FormatSpec ({ format: "timestamp" } etc.) ist JSON-sicher
|
|
157
|
+
// und FieldCondition ({ field, eq/ne } | boolean) bleibt nach
|
|
158
|
+
// JSON.parse(JSON.stringify(app)) deep-equal zum Original.
|
|
159
|
+
const entity = {
|
|
160
|
+
table: "events",
|
|
161
|
+
fields: {
|
|
162
|
+
id: { type: "text" },
|
|
163
|
+
startedAt: { type: "timestamp" },
|
|
164
|
+
status: { type: "text" },
|
|
165
|
+
priority: { type: "number" },
|
|
166
|
+
},
|
|
167
|
+
} as unknown as EntityDefinition;
|
|
168
|
+
|
|
169
|
+
const f = defineFeature("ev", (r) => {
|
|
170
|
+
r.entity("event", entity);
|
|
171
|
+
r.screen({
|
|
172
|
+
id: "list",
|
|
173
|
+
type: "entityList",
|
|
174
|
+
entity: "event",
|
|
175
|
+
columns: [
|
|
176
|
+
"id",
|
|
177
|
+
{ field: "startedAt", renderer: { format: "timestamp" as const } },
|
|
178
|
+
{ field: "priority", renderer: { format: "priority" as const, prefix: "P" } },
|
|
179
|
+
{ field: "status" },
|
|
180
|
+
],
|
|
181
|
+
rowActions: [
|
|
182
|
+
{
|
|
183
|
+
kind: "navigate",
|
|
184
|
+
id: "open",
|
|
185
|
+
label: "Öffnen",
|
|
186
|
+
screen: "detail",
|
|
187
|
+
visible: { field: "status", ne: "archived" },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
kind: "navigate",
|
|
191
|
+
id: "archive",
|
|
192
|
+
label: "Archivieren",
|
|
193
|
+
screen: "archive",
|
|
194
|
+
visible: { field: "status", eq: "open" },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
kind: "navigate",
|
|
198
|
+
id: "always",
|
|
199
|
+
label: "Immer",
|
|
200
|
+
screen: "view",
|
|
201
|
+
visible: true,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
208
|
+
const roundTripped = JSON.parse(JSON.stringify(app));
|
|
209
|
+
|
|
210
|
+
// Vollständige deep-equality — kein Silent-Drop durch JSON.stringify
|
|
211
|
+
expect(roundTripped).toEqual(app);
|
|
212
|
+
|
|
213
|
+
// Explizit: FormatSpec-Felder landen unverändert an
|
|
214
|
+
const screen = roundTripped.features[0]?.screens[0];
|
|
215
|
+
const cols = screen?.columns as Array<{ field?: string; renderer?: unknown }>;
|
|
216
|
+
expect(cols?.find((c) => c.field === "startedAt")?.renderer).toEqual({
|
|
217
|
+
format: "timestamp",
|
|
218
|
+
});
|
|
219
|
+
expect(cols?.find((c) => c.field === "priority")?.renderer).toEqual({
|
|
220
|
+
format: "priority",
|
|
221
|
+
prefix: "P",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Explizit: FieldCondition-Varianten (eq, ne, boolean) landen unverändert an
|
|
225
|
+
const actions = screen?.rowActions as Array<{ id: string; visible?: unknown }>;
|
|
226
|
+
expect(actions?.find((a) => a.id === "open")?.visible).toEqual({
|
|
227
|
+
field: "status",
|
|
228
|
+
ne: "archived",
|
|
229
|
+
});
|
|
230
|
+
expect(actions?.find((a) => a.id === "archive")?.visible).toEqual({
|
|
231
|
+
field: "status",
|
|
232
|
+
eq: "open",
|
|
233
|
+
});
|
|
234
|
+
expect(actions?.find((a) => a.id === "always")?.visible).toBe(true);
|
|
235
|
+
});
|
|
154
236
|
});
|
|
@@ -59,10 +59,31 @@ export function buildAppSchema(registry: Registry): AppSchema {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
const schema = {
|
|
63
63
|
features,
|
|
64
64
|
...(workspaces.length > 0 && { workspaces }),
|
|
65
65
|
};
|
|
66
|
+
|
|
67
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
68
|
+
try {
|
|
69
|
+
const roundTripped = JSON.parse(JSON.stringify(schema));
|
|
70
|
+
if (JSON.stringify(roundTripped) !== JSON.stringify(schema)) {
|
|
71
|
+
// biome-ignore lint/suspicious/noConsole: dev-only assertion
|
|
72
|
+
console.error(
|
|
73
|
+
"[kumiko] buildAppSchema: Output ist nicht JSON-safe — ein Funktions-Renderer oder nicht-serialisierbarer Wert ist in das Schema gerutscht. Details im Diff:",
|
|
74
|
+
schema,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// biome-ignore lint/suspicious/noConsole: dev-only assertion
|
|
79
|
+
console.error(
|
|
80
|
+
"[kumiko] buildAppSchema: JSON.stringify fehlgeschlagen — Schema enthält nicht-serialisierbare Werte.",
|
|
81
|
+
schema,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return schema;
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
function projectEntities(
|
|
@@ -213,7 +213,9 @@ export type {
|
|
|
213
213
|
EntityEditScreenDefinition,
|
|
214
214
|
EntityListScreenDefinition,
|
|
215
215
|
FieldCondition,
|
|
216
|
+
FieldFormatRegistry,
|
|
216
217
|
FieldRenderer,
|
|
218
|
+
FormatSpec,
|
|
217
219
|
ListColumnSpec,
|
|
218
220
|
PlatformComponent,
|
|
219
221
|
RowAction,
|
|
@@ -225,7 +227,12 @@ export type {
|
|
|
225
227
|
ScreenSlots,
|
|
226
228
|
ToolbarAction,
|
|
227
229
|
} from "./screen";
|
|
228
|
-
export {
|
|
230
|
+
export {
|
|
231
|
+
isExtensionEditSection,
|
|
232
|
+
isFormatSpec,
|
|
233
|
+
normalizeEditField,
|
|
234
|
+
normalizeListColumn,
|
|
235
|
+
} from "./screen";
|
|
229
236
|
export type { TargetRef } from "./target-ref";
|
|
230
237
|
export type {
|
|
231
238
|
Subscribe,
|
|
@@ -24,18 +24,38 @@ export type PlatformComponent = {
|
|
|
24
24
|
readonly native?: unknown;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// Built-in value formatters. Apps extend via module augmentation:
|
|
28
|
+
// declare module "@cosmicdrift/kumiko-framework" {
|
|
29
|
+
// interface FieldFormatRegistry { myFormat: { myOption?: string } }
|
|
30
|
+
// }
|
|
31
|
+
// renderer-web handles all built-in keys; unknown app-specific keys fall back
|
|
32
|
+
// to String(value).
|
|
33
|
+
export interface FieldFormatRegistry {
|
|
34
|
+
timestamp: {
|
|
35
|
+
readonly locale?: string;
|
|
36
|
+
readonly dateStyle?: "full" | "long" | "medium" | "short";
|
|
37
|
+
readonly timeStyle?: "full" | "long" | "medium" | "short";
|
|
38
|
+
};
|
|
39
|
+
date: {
|
|
40
|
+
readonly locale?: string;
|
|
41
|
+
readonly dateStyle?: "full" | "long" | "medium" | "short";
|
|
42
|
+
};
|
|
43
|
+
boolean: { readonly trueLabel?: string; readonly falseLabel?: string };
|
|
44
|
+
currency: { readonly symbol?: string };
|
|
45
|
+
priority: { readonly emptyLabel?: string; readonly prefix?: string };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Discriminated union derived from the registry — one variant per key.
|
|
49
|
+
// JSON-safe: no function members, survives buildAppSchema → window.__KUMIKO_SCHEMA__.
|
|
50
|
+
export type FormatSpec = {
|
|
51
|
+
[K in keyof FieldFormatRegistry]: { readonly format: K } & FieldFormatRegistry[K];
|
|
52
|
+
}[keyof FieldFormatRegistry];
|
|
53
|
+
|
|
27
54
|
// Level-2 field renderer (ui-architecture.md §Renderer Customization):
|
|
28
55
|
// - PlatformComponent → platform-specific component from the same feature
|
|
29
|
-
// - string → cross-feature QN reference (resolved
|
|
30
|
-
// -
|
|
31
|
-
|
|
32
|
-
// nützlich für context-aware Renderer (Tier 2.7e-Eagerload nutzt das
|
|
33
|
-
// um aus row._refs den resolved Display-Wert zu lesen). Renderer die
|
|
34
|
-
// nur den value brauchen ignorieren das Argument einfach.
|
|
35
|
-
export type FieldRenderer =
|
|
36
|
-
| PlatformComponent
|
|
37
|
-
| string
|
|
38
|
-
| ((value: unknown, row?: Readonly<Record<string, unknown>>) => string);
|
|
56
|
+
// - string → cross-feature QN reference (resolved at mount-time)
|
|
57
|
+
// - FormatSpec → declarative value formatter, JSON-safe ({ format: "timestamp" } etc.)
|
|
58
|
+
export type FieldRenderer = PlatformComponent | string | FormatSpec;
|
|
39
59
|
|
|
40
60
|
// Declarative field-state condition. Evaluated by the renderer against the
|
|
41
61
|
// current row/form values. Three forms:
|
|
@@ -465,11 +485,35 @@ export type ScreenDefinition =
|
|
|
465
485
|
| ConfigEditScreenDefinition
|
|
466
486
|
| CustomScreenDefinition;
|
|
467
487
|
|
|
488
|
+
// Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
|
|
489
|
+
// authors who branch on the three FieldRenderer variants without manual
|
|
490
|
+
// "format" in renderer checks.
|
|
491
|
+
export function isFormatSpec(r: unknown): r is FormatSpec {
|
|
492
|
+
return (
|
|
493
|
+
typeof r === "object" &&
|
|
494
|
+
r !== null &&
|
|
495
|
+
"format" in r &&
|
|
496
|
+
typeof (r as Record<string, unknown>)["format"] === "string"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
468
500
|
// Collapse the string-shorthand into the object form. Both the boot-validator
|
|
469
501
|
// and (later) ui-core's view-model builder iterate over fields/columns — the
|
|
470
502
|
// helper keeps that loop from growing two branches everywhere.
|
|
471
503
|
export function normalizeListColumn(c: ListColumnSpec): Exclude<ListColumnSpec, string> {
|
|
472
|
-
|
|
504
|
+
const col = typeof c === "string" ? { field: c } : c;
|
|
505
|
+
if (
|
|
506
|
+
typeof process !== "undefined" &&
|
|
507
|
+
process.env.NODE_ENV !== "production" &&
|
|
508
|
+
col.renderer !== undefined &&
|
|
509
|
+
typeof col.renderer === "function"
|
|
510
|
+
) {
|
|
511
|
+
// biome-ignore lint/suspicious/noConsole: dev-only warning
|
|
512
|
+
console.warn(
|
|
513
|
+
`[kumiko] normalizeListColumn: Feld "${col.field}" hat einen Funktions-Renderer — dieser wird von JSON.stringify verworfen. Bitte auf FormatSpec ({ format: "..." }) migrieren.`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return col;
|
|
473
517
|
}
|
|
474
518
|
|
|
475
519
|
export function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string> {
|