Skip to content

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-located

Pro-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/ zu app/web/{client.tsx, shell.tsx} und app/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. Zwei defineFeature() mit demselben Namen werfen Duplicate 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 TenantMembership

schema/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.ts an. 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.ts lebt im Feature-Root, nicht in web/ oder native/. 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/) darf web/index.ts schmal als Public-Re-Export bleiben, und die eigentliche Logik in web/<sub-feature>.ts Files liegen — web/login-screen.tsx, web/session.tsx, web/auth-gate.tsx, etc. Public-Surface ist index.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 mit Duplicate feature
  • schema/ ist beidseitig — kein Server-Code (kein Drizzle-Import, kein Handler-Code) damit der Client-Bundler nichts mitziehen muss
  • Plattform-Trennung über web/ und native/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-locatedfeatures/<name>/__tests__/ neben dem Feature, nicht in zentralem tests/-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/ oder app/utils/ ist klarer
  • App-Feature-Schema in einem anderen App-Feature importieren — würde Coupling reinziehen. Wenn nötig: ins Bundled-Feature ziehen
  • feature.ts von web/ oder native/ 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.ts als 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)

  1. src/app/ anlegen — client.tsx, server.ts, shell.tsx hin
  2. Alte flache src/feature.ts + src/feature-schema.ts aufteilen in src/features/<name>/{feature.ts, schema/<entity>.ts, schema/index.ts, i18n.ts, web/index.ts}
  3. Alte src/client.tsx (i18n inline + Custom-Routing) auflösen:
    • i18n → features/<name>/i18n.ts
    • Custom-Routing-Map → clientFeatures.components der jeweiligen Features
  4. Custom-Components nach features/<name>/web/components/ und web/pages/
  5. app/shell.tsx rendert DefaultAppShell ohne Custom-Routing
  6. 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.