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 files
fougere.config.ts
import '@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 generatesYou write
validation, derived views, tables/metadatanon-trivial business logic
GraphQL types and inputs, trivial CRUDrich invariants, multi-step workflows
projection facades (contract → transport)error policies, consistency decisions
typed clients, composition manifestssensitive 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.

Built with Fougere and Nuxt, hosted on GitHub Pages.