Registries and extension points

Fougere is extended in two ways, and every point of extension is one or the other.

  • By name — a registry. A package registers a word, and config or an entity names it: sources: { main: { type: 'sqlite' } }, protocols: { rest: {} }, format: 'slug'.
  • By type — a port. A class extends a port, or stands in front of it, and what asks for the port receives it: class StripePayment extends Payment, class Audit implements Partial<Dispatcher>.

Registries

RegistryThe word it answersRegistered by
Sourcestype: of a sources: entryadapter-sql, adapter-file, adapter-memory, adapter-env
Protocolsa key of protocols:adapter-rest, adapter-graphql
Statesa way to keep a name of state:state-cookie, state-session
StringFormats, Generators, Verbsa word of an entityyour vocabulary
Axesa fifth axis of a fielda package

A word has one answer per process: the same answer registered twice is harmless, a different one is refused, naming the registry. A package that registers when it is imported says so in its package.json (sideEffects), or a bundler drops the import. Each word also has a type, added to an empty interface of core with declare module — that is what lets the editor refuse a misspelled word: see Interfaces to extend.

Ports

Every port Fougere opens — Dispatcher, Rest, Collect<T>, Present<T>, Env<T>, Storage, Logger — is extended the way your own are: see Port and the reference.

Storage is the one whose realization is handed in rather than written: the source builds a storage per entity, and no class declares it. So a class extending Storage can only stand in front of it — in front of the rows — and it covers every entity of its own frond. No key widens that reach: a link sits beside the code it changes and goes where that code goes, so moving a frond to another process cannot quietly leave a link behind. What must be in every process is a frond every process holds, which an extension brings.

The check stays outside the chain: a row the entity refuses never reaches a link, and what a link receives is the value the schema already parsed.

Seeing what answers

fougere ports

prints every point of this app — each registry with the words it answers, each port with its links in order and the realization they reach — booting the app with no schema check and no seeds. In code, app.extensionPoints() answers the same list.

A transport, and HTTP

Neither is a host port, and neither leaves you without a way in.

A transport is named by the scheme of the address fronds: gives — http://, https://, binding:// for a Cloudflare service binding — and answered by the package that registers it, the way a source type is:

Transports.register('tcp', { open: (address) => mySocketTransport(Number(address.port)) });
// fougere.config.ts
fronds: { sensors: { remote: 'tcp://127.0.0.1:4700' } },

@fougere/transport-http registers http, https and binding. A scheme nothing registered refuses the boot, naming the entry and the schemes this process knows. A host may still swap the transport whole, and then no scheme is read:

boot({ root, createContainer, remoteTransport: (url) => mySocketTransport(url) });

And an outgoing call to a frond behind fronds: already runs through the app's middlewares before it reaches the wire, so retrying, timing or logging one is a host middleware (app.use), not a transport:

app.use(async (ctx, next) => { /* ctx.address, ctx.operation */ return next(); });

What that does not reach is the envelope itself — the encoding, the headers, the connection. For those you are the transport, and you swap it.

HTTP is the engine's from start to finish: the app mounts on it with answer(app, request), and the engine's own middleware stands around it. Leaving a route out is a middleware answering before answer does:

hono.delete('/api/*', (c) => c.text('Method Not Allowed', 405));
hono.all('/api/*', async (c) => (await answer(app, c.req.raw)) ?? c.notFound());

Where an op answers is the frond's to say, in its frond.config.ts — see surfaces for the three parts of a URL and who sets each one.

Built with Fougere and Nuxt, hosted on GitHub Pages.