Section Loading
How SectionFactory discovers, imports, validates, caches, and serves section configs, and how section classes are built.
Code: packages/nextjs-cms/src/core/factories/section-factory-with-jiti.ts, packages/nextjs-cms/src/core/sections/
From file to config
SectionFactory.loadAllSections()
├─ glob('**/*.section.ts', { cwd: sections.path })
├─ for each file:
│ ├─ jiti(absPath).default transpile + import at runtime
│ ├─ reject arrays / missing default export
│ ├─ wrap build() so instances know their configFile
│ └─ collect errors per file (import errors, bad extensioned imports...)
├─ validate names (unique, case-insensitive) → section-name-validation.ts
├─ strict mode? any error → return []
└─ cache the promise (allSectionsPromise) until files or config changeThe section helpers (hasItemsSection() etc.) already validated each config with Zod when the file executed, so by the time the factory sees a config it's structurally sound.
Public API
| Method | Used by | Returns |
|---|---|---|
getSections(type?) | Admin internals | All section configs (optionally filtered by type), with logging and the dev watcher |
getSectionsSilently(type?) | CLI | Same, without logging or watchers |
getSectionsForAdmin({ type, admin }) | Navigation, pages | Sections the admin has a privilege for, grouped by type, plus fixed pages (admins) |
getSection({ name, type }) | Internals | One config |
getSectionForAdmin({ name, type, admin }) | Pages, submit | One config, only if the admin has the privilege (and requiredRole operation) |
All return configs. Callers call .build() to get a Section instance when they need fields.
Section classes
Section (abstract) core/sections/section.ts
├── SimpleSection type 'simple' title: MultilingualString
├── HasItemsSection type 'has_items' headingField, search, browse, coverPhotoField...
└── CategorySection type 'category' depth, allowRecursiveDelete (+ parent_id, level)The Section constructor:
- Resolves the gallery config with default column names.
- Injects the auto-increment
ididentifier whendb.identifieris missing (prepending it tofieldsand the first field group). - Verifies the identifier is a text or number field.
- Validates unique field names.
- Resolves
db(defaultsprimaryKeyto[identifier], adds the identifier tounique).
Fields are not built in the constructor. Call section.buildFields() to create Field instances (section.fields, section.fieldGroups); accessing fields before that throws a descriptive error.
CategorySection appends parent_id (matching the identifier type) and level number fields when depth > 1, on a copied array so repeated build() calls don't accumulate them.
Helper functions vs. classes
export function hasItemsSection(section: HasItemsSectionOptions): HasItemsSectionConfig {
// 1. optionsSchema.safeParse(section) → throws "[Section: name]: ..." with a prettified Zod error
// 2. split fields vs. field groups (first entry decides)
// 3. validateFieldNamesForSection() → reserved `id`, duplicates
// 4. return { ...section, fields, fieldGroups, type, build: () => new HasItemsSection(...) }
}Keep this split when adding options: validation in the helper (runs once at load), resolution in the class (runs per build).
Hot reload
In development, startWatcher() watches **/*.section.ts with chokidar. On change it:
- Resets the jiti instance (jiti's module cache can hold stale transpiled code).
- Clears the cached sections promise.
- Rewrites the hot marker
components/form/helpers/_section-hot-reload.jsin the app, so Next.js Fast Refresh re-renders open forms.
The CLI uses getSectionsSilently(), which never starts a watcher.
Why jiti's sync API
The loader calls jiti(absPath) (the synchronous, deprecated API) rather than await jiti.import() because the async API doesn't work under Turbopack. Section files are small, so the sync transpile cost is negligible and cached (fsCache, moduleCache).
Error reporting
Errors are collected per file into sectionProcessingErrors / sectionFetchingErrors and printed together, with targeted hints. For example, importing another section with an extension (./other.section.ts) is detected and explained, because the bundled import can't resolve it.