@dokebi/module
v0.4.1
Published
Feature modules for Dokebi apps: controllers, models, views and routes in one folder mounted under a prefix.
Maintainers
Readme
@dokebi/module 0.4.1
An optional package for Node >=24.0.0 and core ^0.4.0 (public on npm). A module is one feature as an MVC unit of business code (D-0022, D-0032). It keeps controllers, services, models, views, routes and sockets in one folder, and apps mount it under a prefix. It is not an HTTP-only bundle, so business code without a web side (job processing, event subscriptions) is a module too.
Folder
src/shared/modules/<module>/
module.ts export default defineModule({ ... })
routes.ts routes registered when an app mounts the module
controllers/ this module's controllers (created once in the module's setup)
models/ this module's models (plain code)
views/default/ (modules with pages only) EJS views; other templates hold only changed files
sockets/ (when needed) defineSocketdokebi module add <name> creates the skeleton and adds it to the generated registration code. Create a module without pages with --no-views; its routes then return JSON. dokebi module remove <name> removes only the registration and leaves the folder. dokebi module list shows the list. dokebi add @dokebi/module only creates the src/shared/modules/ folder.
defineModule
export default defineModule({
name: 'board', // the folder name; the ID is dokebi-module.board
requires: [db, other], // plugin instances, descriptors, other modules
controllers: [postsController], // created once with the module context
contributes: [mcpTools], // points of plugins it contributes to; module.contribute in setup
views: new URL('./views/', import.meta.url), // default/ required; template names get the prefix module.board
routes(route, module) { // registered per app (when mounted). page = { app, module, base, view, inertia }
route.get('/', (request, reply, page) =>
page.view.reply(reply, 'index', module.controller(postsController).list(page)));
},
sockets: (module) => [boardSocket(module)], // registered per app under the prefix; tenant is <namespace>:<tenant>
events: (module) => [boardStream(module)], // (0.1.3) SSE routes (@dokebi/sse); same rules as sockets
setup(module) { return api; }, // optional; the return value is the API of whoever requires this module
start(module) {}, drain(module) {},
});module(ModuleContext): PluginContext plusmodule(the name) andcontroller(token).- It reads only the declared
requires. - When a controller factory passes a freshly built descriptor object, the declared descriptor with that name is found.
- It reads only the declared
version: an exact semver, default'0.1.0'. Requires with duplicate names are refused.views: adds a template-ejs dependency automatically. Withoutviews/default/the start fails.controllers: uses @dokebi/controller (optional peer). Callingmodule.controller()without controllers fails withdeclares no controllers.setup: optional. The oldersetup(context)form still works.contributes(D-0130): the points of other plugins this module contributes to (for examplemcpToolsof @dokebi/mcp).setupcallsmodule.contribute(point, value)with objects that use the module's own services; core sets the module up before every collector of the point. A point that nothing collects drops its values with warnPOINT_NOT_COLLECTED; an undeclared point isUNDECLARED_POINT; modules never collect. See AI.md §3a.Shared APIs: APIs that must answer on every domain are registered directly with router or web in setup, for example the playground's
/api/*.Mounting on apps:
defineApp({ modules: { '/board': board } }). Routes and sockets answer only on that app's domains and prefix. A mounted module's API is readable withapp.get(board)without listing it in the app'srequires. Details are in @dokebi/app.View lookup order:
<app>/<template>/<module>/…<app>/default/<module>/…- the module's
<template>/… - the module's
default/… - the app's views (layouts and partials)
- shared
src/shared/views
So an app can change a single file of a module page, for example
src/apps/<app>/views/christmas/<module>/heading.ejs.Inertia apps: the module produces props with
page.inertia.render(request, reply, 'Component', props). The app keeps the Vue pages inviews/<template>/Pages/.Sockets and SSE: the return types of
socketsandevents(0.1.3) are defined by shape only (ModuleSocketRoute,ModuleEventRoute), so projects that do not use them need neither @dokebi/websocket nor @dokebi/sse. The mounting app registers them under its namespace rules (D-0061): the tenant is<namespace>:<authenticated tenant>, and an authenticatednamespacemust be in that app's list.moduleMount(plugin)andMODULE_MOUNT: how the app plugin reads the mounting information. App code never needs them.
Group plugin
createPlugin({ app: app.view, entry: { modules: [...] } }) groups several modules into a dependency graph and exposes the frozen names (plugin contract v2, D-0028).
- Generated code: passes the default exports of the modules registered with
dokebi module addto this entry. Projects with the old entry filesrc/shared/modules/index.tsget them appended after itsmoduleslist. - Accepted modules: only what
defineModulemade (IDs starting withdokebi-module.). Duplicates areINVALID_PLUGIN_INPUT. A single module may also be registered directly with app.register. - Lifecycle: setup registers routes, start prepares to run, drain stops new input and waits for work in progress, and context.defer cleans up after the whole drain.
- Drain order: reverse dependency order. A module's drain starts after the drains of the modules that depend on it have finished. Unrelated modules drain at the same time.
- Instances: module names cannot repeat. One module instance is used by one Application only.
- Not provided: automatic discovery, dynamic import, hot reload and folder scanning while running. The CLI writes registrations into the generated code.
- Configuration: the YAML settings are an empty object.
Examples:
- examples/26-scaffold: repeated checks of generation, mounting and overriding
- examples/25-playground: eight feature modules
examples/22-web/main.ts: assembled in code
See the AI guide for errors and cautions.
Service composition
Use Controller -> Service -> Model for application data. Controllers handle HTTP input/output;
services provide data and coordinate workflows through models; models own persistence.
Generated app/module starters include this chain. Keep services free of request/reply and
rendering objects, and reuse them from jobs. Compose dependencies once during setup.
App-specific services belong in src/apps/<app>/services, cross-app services in
src/shared/services, and module services in src/shared/modules/<module>/services.
Shared services must not import app-specific code. These are ordinary application classes,
not a new framework plugin or service registry. Existing projects are not moved automatically.
