App-Struktur — Convention für Kumiko-Apps und Bundled-Features
Wie ein Kumiko-Repo intern aufgebaut sein soll. Die Convention ist feature-modular + plattform-getrennt:
- Jedes Feature lebt in einem eigenen Ordner mit allem drin (Schema, Server, i18n, Plattform-Plugins).
- Plattform-spezifischer UI-Code (React-DOM, React-Native) lebt in
web/bzw.native/Subdirs — Server- und Schema-Code bleibt plattform-neutral im Feature-Root.
Die Convention gilt für App-Features (samples/apps/*/src/features/)
und für Bundled-Features (packages/bundled-features/src/<name>/).
Anker-Implementation: samples/apps/showcase/.
Ordner-Tree
samples/apps/<app-name>/├── public/│ ├── index.html│ └── favicon.ico ← static-files, served unter Root-URL└── src/ ├── app/ ← App-Level: Boot + Shell + globale i18n │ ├── client.tsx ← Browser-Entry — createKumikoApp() │ ├── server.ts ← Server-Boot — runDevApp() │ ├── shell.tsx ← AppShell-Component │ ├── i18n.ts ← (optional) app-globale Keys │ └── assets/ ← (optional) app-globale Bilder/Logo │ └── features/ ← Pro Feature ein Ordner └── <feature-name>/ ├── index.ts ← Re-exports: feature, web (+ später native) ├── feature.ts ← defineFeature() — SERVER-ONLY ├── i18n.ts ← Translations (de + en) — plattform-neutral ├── constants.ts ← (optional) Feature-Konstanten ├── handlers/ ← (optional) wenn handlers in feature.ts unübersichtlich ├── schema/ ← Entity- + Screen-Definitionen — BEIDSEITIG │ ├── index.ts ← barrel — re-exports │ ├── <entity-1>.ts │ └── <entity-2>.ts ├── web/ ← React-DOM Plattform-Plugin │ ├── index.ts ← ClientFeatureDefinition (Web) │ ├── components/ ← (optional) Custom-Components für custom-Screens │ ├── pages/ ← (optional) Custom-Screen-Pages │ └── assets/ ← (optional) feature-spezifische Bilder ├── native/ ← (zukünftig) React-Native Plattform-Plugin │ └── index.ts ├── testing.ts ← (optional) Test-Helpers für andere Features └── __tests__/ ← Feature-Tests, co-locatedPro-Datei: was gehört wo
app/client.tsx
Browser-Entry. Soll dünn sein — sammelt nur die ClientFeatures und
ruft createKumikoApp(). Kein i18n-Bundle inline, keine
Custom-Routing-Map. Beispiel:
import { createKumikoApp } from "@kumiko/renderer-web";import { itemsClient } from "../features/items";import { demosClient } from "../features/demos";import { AppShell } from "./shell";
createKumikoApp({ shell: AppShell, clientFeatures: [itemsClient, demosClient],});app/server.ts
Server-Boot. Auch dünn — nur runDevApp() mit der Feature-Liste.
app/shell.tsx
Die AppShell-Component, die DefaultAppShell mit brand,
sidebarActions, sidebarFooter füllt. Kein Custom-Screen-Routing
hier — das macht das Framework via clientFeatures.components.
Multi-Plattform: wenn die App sowohl Web als auch Native bedient, kann
app/zuapp/web/{client.tsx, shell.tsx}undapp/native/{client.tsx}aufgeteilt werden. Solange nur Web läuft: flach lassen.
features/<name>/feature.ts
defineFeature()-Aufruf mit Handlern, Entities, Screens, Navs.
Server-only — importiert @kumiko/framework/engine Sachen die
DB/Drizzle/Server-Code mit reinziehen. Soll NIE von web/ oder
native/ importiert werden.
import { defineEntityWriteHandler, defineFeature } from "@kumiko/framework/engine";import { itemEntity, itemListScreen } from "./schema";
export const itemsFeature = defineFeature("<feature-name>", (r) => { r.entity("item", itemEntity); r.writeHandler(defineEntityWriteHandler("item:create", itemEntity, open)); r.screen(itemListScreen); r.nav({ id: "item-list", screen: "item-list", label: "..." });});Wichtig: Der String in
defineFeature("<name>", ...)muss appweit eindeutig sein. ZweidefineFeature()mit demselben Namen werfenDuplicate feature: "<name>"beim Boot. Convention: Feature-Name = Ordner-Name (features/items/→"items").
features/<name>/schema/
Entity- + Screen-Definitionen, eine Datei pro Entity. Beidseitig
importierbar — kein Server-Code (kein Drizzle-Import, keine Handler).
Server liest’s aus feature.ts, Client liest’s indirekt über
window.__KUMIKO_SCHEMA__ (Server injected das beim Boot).
schema/├── index.ts ← barrel: re-exports aus jedem Entity-File├── tenant.ts ← Entity + Screens für Tenant└── membership.ts ← Entity + Screens für TenantMembershipschema/index.ts:
export { tenantEntity, tenantListScreen } from "./tenant";export { membershipEntity } from "./membership";Single-Entity-Feature: auch wenn das Feature heute nur eine Entity hat, lege
schema/<entity>.ts+schema/index.tsan. Konsistente Struktur, keine „wenn/dann”-Ausnahmen, einfache Erweiterung wenn weitere Entities dazukommen.
features/<name>/i18n.ts
Translations als Objekt. Convention: feature-prefix <feature>: für
feature-eigene Keys, plus die globalen screen:<id>.title Keys für die
Top-Action-Bars.
export const itemsTranslations: TranslationsByLocale = { de: { "items:nav.list": "Items", "items:entity:item:field:title": "Titel", "screen:item-list.title": "Items", }, en: { ... },};Plattform-Neutral: Strings sind Strings.
i18n.tslebt im Feature-Root, nicht inweb/odernative/. Beide Plattform- Plugins importieren das gleiche Bundle.
features/<name>/web/index.ts
ClientFeatureDefinition mit Translations und (optional) Components-Map
für die Web-Plattform.
Sub-Files in
web/erlaubt: bei größeren Plattform-Plugins (z.B.auth-email-password/web/) darfweb/index.tsschmal als Public-Re-Export bleiben, und die eigentliche Logik inweb/<sub-feature>.tsFiles liegen —web/login-screen.tsx,web/session.tsx,web/auth-gate.tsx, etc. Public-Surface istindex.ts, der re-exportiert was Apps importieren sollen.
import type { ClientFeatureDefinition } from "@kumiko/renderer-web";import { itemsTranslations } from "../i18n";import { ItemDetail } from "./components/item-detail";
export const itemsClient: ClientFeatureDefinition = { name: "<feature-name>", // matcht featureName in feature.ts translations: itemsTranslations, components: { "item-detail": ItemDetail, // für r.screen({ type: "custom" }) },};features/<name>/web/components/
Custom-React-DOM-Components für Screens vom Typ "custom". Werden in
web/index.ts als components-Map registriert. Framework macht das
Routing automatisch (siehe KumikoScreen + CustomScreensProvider).
features/<name>/web/pages/
(Convention für komplexere Sample-/Demo-Features.) Custom-Screen-
Top-Level-Components, eine Datei pro Page. Wenn das Feature nur 1-2
Custom-Screens hat, reicht web/components/ — pages/ wird sinnvoll
ab 3+ Custom-Screens oder wenn Pages eigene Sub-Components brauchen.
features/<name>/web/assets/
Feature-spezifische Bilder/Icons fürs Web, per Import gebundlet. Bei Feature-Extract ins Bundled-Package mit umgezogen.
features/<name>/index.ts
Public-Surface. Re-exportiert feature und <plattform> clients —
schema/, handlers/, web/components/ bleiben internal.
export { itemsFeature } from "./feature";export { itemsClient } from "./web";// Sobald native existiert:// export { itemsNativeClient } from "./native";Conventions
- Feature-Name = Ordner-Name — mechanisch ableitbar, keine Diskussion
defineFeature("<name>", ...)muss eindeutig sein — sonst Boot-Crash mitDuplicate featureschema/ist beidseitig — kein Server-Code (kein Drizzle-Import, kein Handler-Code) damit der Client-Bundler nichts mitziehen muss- Plattform-Trennung über
web/undnative/—web/darf raw HTML/Tailwind nutzen,native/darf React-Native-APIs. Server-Code und Schema bleiben im Root, plattform-neutral - Custom-Screens via
clientFeatures.components— kein hand-gerolltes Routing im AppShell, Framework macht das automatisch - Tests co-located —
features/<name>/__tests__/neben dem Feature, nicht in zentralemtests/-Dir
Anti-Patterns
shared/anlegen ohne dass was ECHT app-übergreifend ist — Müllhalden-Risiko. Default ist „nicht hier reintun, wenn nötig später”. Wenn doch nötig:app/lib/oderapp/utils/ist klarer- App-Feature-Schema in einem anderen App-Feature importieren — würde Coupling reinziehen. Wenn nötig: ins Bundled-Feature ziehen
feature.tsvonweb/odernative/importieren — pulled Server-Code in den Plattform-Bundle. Geht heute durch Tree-Shaking durch, ist aber eine Falle wenn das Feature später Hooks/Lifecycle dazubekommt<name>-feature.tsals Datei-Name (Naming-Verdopplung mit dem Ordnernamen). Convention:feature.ts. Bundled-Features nutzen heute noch das alte Pattern und werden schrittweise migriert
Migration-Checkliste (von flat zu feature-modular)
src/app/anlegen —client.tsx,server.ts,shell.tsxhin- Alte flache
src/feature.ts+src/feature-schema.tsaufteilen insrc/features/<name>/{feature.ts, schema/<entity>.ts, schema/index.ts, i18n.ts, web/index.ts} - Alte
src/client.tsx(i18n inline + Custom-Routing) auflösen:- i18n →
features/<name>/i18n.ts - Custom-Routing-Map →
clientFeatures.componentsder jeweiligen Features
- i18n →
- Custom-Components nach
features/<name>/web/components/undweb/pages/ app/shell.tsxrendertDefaultAppShellohne Custom-Routing- Feature-Namen prüfen: jeder
defineFeature("<name>", ...)muss eindeutig sein
Wann ein Feature in ein eigenes Package ziehen?
App-Feature in src/features/<name>/ bleibt — bis:
- Es von ≥2 Apps gebraucht wird
- Es als Lib-Surface wiederverwendet werden soll
Dann: Ordner verschieben nach packages/bundled-features/src/<name>/,
package.json ergänzen, App importiert per workspace:*. Die interne
Struktur bleibt gleich — keine Refactor-Schritte am Code.