How the scan reads a project
You never register a class. The scan reads where a file sits and what it is, writes the statement
every host boots from (fronds.ts), and refuses what does not hold. These are the directories it reads:
fronds/blog/
vocabulary/ ← words this frond adds to the declaration — read FIRST
entities/ ← the declarations
handlers/ ← the operations (handlers/<surface>/ = a named audience)
rules/ ← what the domain computes with nothing injected
services/ ← every class injected by type: a repository, a collector, a presenter
seeds/ ← data created at startup
extensions/ ← what rises and falls with the process
versions/ ← what its shapes used to be (`fougere freeze`)
The scanner recognizes a frond through these directories, in a directory under fronds/.
A second domain is added beside the first, and the first does not move.
These directory names are the only place where a name is the declaration, so they are the only
names a project can change. conventions: in fougere.config.ts names what differs
and nothing else:
export default defineFougere({
conventions: {
scope: '@domains', // default: '@fronds'
fronds: 'domains', // default: 'fronds'
dirs: { entities: 'models' }, // the other seven keep their names
},
})
It is read before the fronds are discovered, which is what lets it name the scope they
are found under — so nothing in fougere.config.ts may import @fronds/*. The scan, the boot,
the CLI and Nuxt follow it. @fougere/vite and @fougere/next do not yet.
TypeScript does not: @fougere/core/tsconfig is a file, and it says fronds and @fronds. A
project that changes scope or fronds writes both in its own tsconfig.json, and a change to
dirs asks nothing of it:
{
"extends": "@fougere/core/tsconfig",
"include": ["domains", "fougere.config.ts", ".fougere/names.generated.d.ts"],
"compilerOptions": {
"paths": {
"@fronds/facade": ["./.fougere/facade.generated.ts"],
"@domains/*": ["./domains/*", "./remotes/*"]
}
}
}
include and paths replace what they inherit, so each is written whole. @fronds/facade keeps
its name whatever the scope. Without the include, tsc passes having read none of domains/.
demos/conventions17 filesimport '@fougere/adapter-memory';
import { defineFougere } from '@fougere/core';
export default defineFougere({
conventions: {
scope: '@shop',
fronds: 'domains',
dirs: {
entities: 'models',
handlers: 'endpoints',
services: 'providers',
seeds: 'fixtures',
rules: 'logic',
versions: 'history',
vocabulary: 'terms',
extensions: 'plugins',
},
},
sources: { main: { type: 'memory' } },
});
Which TypeScript
Your project checks its types with the TypeScript it installs, 7 in the starter. The scan carries its own TypeScript 6, because it reads types through a program and TypeScript 7 does not offer that API as stable yet. The two never meet.
Naming and importing a frond
The directory name is the frond's name: fronds/blog/ is @fronds/blog, so a page or another
Frond writes import Post from '@fronds/blog/entities/Post.js' with no file to add. The name also
keys the entity registrations and any fronds: entry.
@fronds/<name> is read where TypeScript reads it — the paths that the project's tsconfig.json
inherits from @fougere/core/tsconfig: fronds/<name>, then remotes/<name>, where fougere sync
writes a frond another process serves. Each host reads the same rule, so a frond created while the
server runs resolves at its first import, and no install links it anywhere.
The one thing the directory cannot say is a name that differs from it. That is a paths entry of
the project's tsconfig.json, beside the rule it would otherwise inherit:
{
"extends": "@fougere/core/tsconfig",
"compilerOptions": {
"paths": {
"@fronds/blog/*": ["./fronds/blog-v2/*"],
"@fronds/*": ["./fronds/*", "./remotes/*"]
}
}
}
fronds/blog-v2/ then stays registered as blog, keeping its entity keys, its imports and its
fronds: entries. The rule is written again because paths replaces what it inherits. An app whose
tsconfig.json states its own paths (Vite, Next) takes the same entry; Nuxt is told by
@fougere/nuxt.
services/ is scanned for every class injected by type, told apart by what it extends. Every
entity already has a default repository: a handler extending Repository<Entity> receives it automatically, and
other code asks for Repository<Entity> or a declared <Entity>Repository. Direct
Storage injection into a handler, presenter or collector is refused at boot. Write a
repository when a query deserves a name or several entities form an aggregate; write a
service for a source that is not storage at all (an external API or a search index).
When one of those classes extends another, the base becomes a
port: the handler declares the base and receives the
implementation, and which one answers is a line in fougere.config.ts — never in the
Frond.
What is written, what is derived
| Fougere generates | You write |
|---|---|
| validation, derived views, tables/metadata | non-trivial business logic |
| GraphQL types and inputs, trivial CRUD | rich invariants, multi-step workflows |
| projection facades (contract → transport) | error policies, consistency decisions |
| typed clients, composition manifests | sensitive inter-Frond orchestration |
A service layer is optional: a handler can hold the logic or hand it to a service.
To boot without the scan, from a fronds.ts you write: Boot without the compiler.