@cosmicdrift/kumiko-framework 0.35.0 → 0.37.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.37.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(
|
|
@@ -227,7 +227,12 @@ export type {
|
|
|
227
227
|
ScreenSlots,
|
|
228
228
|
ToolbarAction,
|
|
229
229
|
} from "./screen";
|
|
230
|
-
export {
|
|
230
|
+
export {
|
|
231
|
+
isExtensionEditSection,
|
|
232
|
+
isFormatSpec,
|
|
233
|
+
normalizeEditField,
|
|
234
|
+
normalizeListColumn,
|
|
235
|
+
} from "./screen";
|
|
231
236
|
export type { TargetRef } from "./target-ref";
|
|
232
237
|
export type {
|
|
233
238
|
Subscribe,
|
|
@@ -31,8 +31,15 @@ export type PlatformComponent = {
|
|
|
31
31
|
// renderer-web handles all built-in keys; unknown app-specific keys fall back
|
|
32
32
|
// to String(value).
|
|
33
33
|
export interface FieldFormatRegistry {
|
|
34
|
-
timestamp:
|
|
35
|
-
|
|
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
|
+
};
|
|
36
43
|
boolean: { readonly trueLabel?: string; readonly falseLabel?: string };
|
|
37
44
|
currency: { readonly symbol?: string };
|
|
38
45
|
priority: { readonly emptyLabel?: string; readonly prefix?: string };
|
|
@@ -478,11 +485,35 @@ export type ScreenDefinition =
|
|
|
478
485
|
| ConfigEditScreenDefinition
|
|
479
486
|
| CustomScreenDefinition;
|
|
480
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
|
+
|
|
481
500
|
// Collapse the string-shorthand into the object form. Both the boot-validator
|
|
482
501
|
// and (later) ui-core's view-model builder iterate over fields/columns — the
|
|
483
502
|
// helper keeps that loop from growing two branches everywhere.
|
|
484
503
|
export function normalizeListColumn(c: ListColumnSpec): Exclude<ListColumnSpec, string> {
|
|
485
|
-
|
|
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;
|
|
486
517
|
}
|
|
487
518
|
|
|
488
519
|
export function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string> {
|