@dokebi/template-ejs
v0.4.3
Published
Precompiled EJS templates for Dokebi apps: layouts, partials and per-app scopes.
Maintainers
Readme
@dokebi/template-ejs 0.4.3
An optional package for Node >=24.0.0 and core ^0.4.0 (public on npm). The registered ID is dokebi-template-ejs. API-only apps do not need it. It replaces the earlier names @dokebi/template, dokebi-template and plugins/dokebi-template. In an existing CLI project, remove the old plugin, add the new package, and change imports and explicit dependency IDs. There is no automatic alias.
Usage
It follows plugin contract v2 (D-0028): createPlugin({ app: app.view, options?, entry? }), where app is required. The old top-level templates, defaultLayout and engine inputs are gone. Options come from options or app.config['dokebi-template-ejs']; code input goes through entry.
import { createPlugin } from '@dokebi/template-ejs';
const view = createPlugin({
app: app.view,
options: { defaultLayout: 'layouts/base' },
entry: {
directory: new URL('./views/', import.meta.url), // optional: reads the .ejs files below it recursively
templates: {
'layouts/base': '<html><body><%- data.$body %></body></html>',
'layouts/member': { source: '<main><%- data.$body %></main>', layout: 'layouts/base' },
heading: '<h1><%= data.name %></h1>',
member: { source: '<%- await data.$include("heading") %>', layout: 'layouts/member' },
},
},
});
// Declare the dependency in setup and read it with context.get(view).
// await api.render('member', { name: '<Alice>' });
// handler: (request, reply) => api.reply(reply, 'member', { name: 'Alice' })Options
dokebi-template-ejs:
defaultLayout: layouts/main # optional
extension: .ejs # default .ejs
maxTemplates: 256 # 1..4096, file and code templates together
maxTemplateBytes: 1048576 # 1..16777216, bytes of one templateInvalid options are INVALID_PLUGIN_OPTIONS in the factory.
entry
{ directory?: URL; templates?: Record<string, string | { source, layout? }>; engine?: TemplateEngine }. It accepts a plain object or an entry module namespace; other keys are INVALID_PLUGIN_INPUT.
directorymust be afile:URL. Setup reads everything below it recursively and registers only files with the configuredextension. The name is the/-separated relative path without the extension (layouts/main.ejs→layouts/main). Files with other extensions (such as the entry fileindex.ts) are ignored.- Symbolic links are not skipped but refused with a start failure (the directory itself included). Exceeding the count or size limits, bad names, folders deeper than 32 levels, files that are not UTF-8 and a file colliding with a
templatesname also fail the start. When the start is cancelled (context.signal), reading stops. - Layouts are chosen only through options or entry, never by a directive inside the file. File templates follow
defaultLayout; for a different layout per template, put it intemplatesas{ source, layout }.
Rendering
render(name, data, { layout? }?) returns a Promise of the HTML string. reply(reply, name, data, options?) renders, calls reply.type('text/html; charset=utf-8') and reply.send(html), and returns the same reply. To avoid depending on Fastify types it accepts any object shaped { type(v: string): unknown; send(p: string): unknown }. When the render fails, type and send are not called.
Layout priority is the call option → the page definition → defaultLayout. false turns the layout off. The chosen layout nests only through its own layout definition and never applies defaultLayout again. Rendering a name that is used as a layout (defaultLayout and every definition's layout) directly does not apply defaultLayout.
Template syntax (contract v2 change): data.$body holds the result of the previous render. await data.$include(name, locals?) renders a registered partial without a layout; locals override the current data. The old data.body and data.include are plain data keys now. Data or locals with a top-level key starting with $ are refused with INVALID_DATA (since 0.4.0 every failure is a TemplateError: a DokebiError with code, plugin and fix). Each name is compiled once in setup. Missing layouts and definition cycles fail the start; unregistered names, partial recursion, more than 32 levels and calls after shutdown fail the render.
The default engine uses EJS 7.0.1 with strict mode and data locals. <%= escapes HTML and <%- prints raw output. Register only trusted templates: EJS is JavaScript, not a sandbox. File reads by the native include() are blocked; use the registered partial API. There is no file watching or dynamic loader while running.
The engine can be replaced with TemplateEngine.compile(source) => (data) => string | Promise<string>. No inheritance is needed. A replacement engine's escaping, security and resource policy are its author's responsibility. Create a new instance per Application. Return or await render Promises in HTTP handlers, or wait for them in the owning module's drain; the registry is cleaned up after the whole drain.
Install
dokebi add uses a static install declaration to create the entry file src/shared/views/index.ts (export const directory = new URL('./', import.meta.url);) only. Standard EJS app creation in CLI 0.4.9 adds the shared layout and its default setting. The generated code passes the entry module as entry, so every .ejs file under src/shared/views is registered.
The runnable example 22 verifies an API-only setup and EJS layouts and partials. Run the tests with npm test -w @dokebi/template-ejs.
Adding folders and template scopes (D-0030)
addDirectory(url, prefix): during setup, adds the templates below a folder under the names<prefix>/<relative path>(for example an app's views). Compilation happens once in the plugin's start, sorenderandreplywork only after start and are refused before.scope(prefixes): returns a renderer that looks up names in prefix order and finally without a prefix. The rendered name, the layouts (defaultLayout, defined layouts,options.layout) and$includeall follow this order. Example:scope(['shop/christmas', 'shop/default'])finds files missing from christmas in default, then in the shared folder.defaultLayoutis valid when the name exists without a prefix or under any scope.
See the AI guide for error codes and the view engine contribution point.
Since 0.4.3, installing the engine creates only its entry. CLI 0.4.9 creates the shared default layout when an EJS app is chosen; Inertia and mail-only installations need no web layout. Existing templates and layout settings are preserved on upgrade.
