---
title: "Codegen (indexAdoniaResources)"
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Codegen (indexAdoniaResources)

Implements TECH_SPEC §14 following the codegen contract (worker-isolated evaluation).

Register the hook in `adonisrc.ts`, after AdonisJS' own `indexEntities()`:

```ts
import { indexEntities } from '@adonisjs/core'
import { defineConfig } from '@adonisjs/core/app'
import { indexAdoniaResources } from '@adonia/devtools/hooks'

export default defineConfig({
  hooks: {
init: [indexEntities({ transformers: { enabled: true } }), indexAdoniaResources()],
  },
})
```

It runs when the assembler boots the dev server, test runner, or bundler, and
regenerates under `node ace serve --hmr` when a file under `app/adonia/resources/`
or `app/adonia/panels/` changes.

## Artifacts

All three land under `.adonisjs/adonia/` and are written as one set.

### `resources.ts`

The typed registry the panel consumes: `slug → () => import(module)`, sorted by
slug and checked with `satisfies AdoniaResourceRegistry` (an alias of
`ResourceRegistryInput` from `@adonia/core`).

Its first line is `/// <reference path="./descriptor.d.ts" />`. That is
load-bearing: AdonisJS' shared tsconfig includes `**/*` plus
`./.adonisjs/server/**/*`, and neither glob reaches `.adonisjs/adonia`, so the
ambient types below would never enter the program. The registry *is* in the
program (your panel imports it), and TypeScript follows the reference from
there — no tsconfig edit required in the host app.

### `descriptor.d.ts`

One ambient namespace per resource, derived by *evaluating* each schema in the
worker (the codegen contract — evaluation, not static parsing, is what sees computed and
conditional fields):

<!-- @sample-preamble
interface RowAbilities {
  create: boolean
  delete: boolean
  edit: boolean
  forceDelete: boolean
  restore: boolean
  view: boolean
  viewList: boolean
}
-->

```ts
declare namespace AdoniaGen {
  namespace Post {
interface Row {
  can: RowAbilities
  id: string | number
  status: 'draft' | 'review' | 'published'
  title: string
}
interface FormState {
  publishedAt: string | null
  status: 'draft' | 'review' | 'published'
  title: string
}
  }
}
```

- **`Row`** — protocol §6's reserved `id`/`can` plus every index column key,
  verbatim (dotted keys like `'author.fullName'` are quoted, never split).
  Columns come from an explicit `table()` when declared, else from §7.5
  derivation.
- **`FormState`** — every field projected into the `form` context (§7.4). A
  field is `| null` unless it is `required()` and not `nullable()`, because
  create-mode state is explicit `null` for every key without a default
  (protocol §7).
- `RowAbilities` and `JsonValue` are emitted alongside the namespaces, so the
  file typechecks standalone.

### `manifest.json`

Consumed by [`adonia:doctor`](/doctor) and the docs generator:

```json
{
  "protocolVersion": 1,
  "generatedAt": null,
  "resources": [
{
  "slug": "posts",
  "class": "PostResource",
  "module": "#adonia/resources/post_resource",
  "panels": ["admin"],
  "columns": ["status", "title"],
  "fields": ["publishedAt", "status", "title"],
  "actions": [],
  "widgets": []
}
  ],
  "panels": [
{ "id": "admin", "path": "/admin", "domain": null, "guard": "web", "resources": ["posts"] }
  ]
}
```

`actions` and `widgets` are always `[]` until M2 (§22); the keys exist so
consumers read one stable shape across milestones. `generatedAt` is always
`null` — a timestamp would make the file churn on every regen.

`node ace add @adonia/core` seeds all three artifacts (§3 step 3b), including
an empty manifest on an app with no resources yet. That is deliberate: "the
file is missing" and "the file says there is nothing" are different diagnoses,
and only the first is a broken install. The hook owns regeneration from the
first assembler run on and never has its output overwritten by the installer.

## Determinism

§14 requires committable, CI-diffable output, so generation is byte-stable:
resources sorted by slug, interface members and name lists sorted
alphabetically by UTF-16 code unit (never locale), LF line endings, exactly one
trailing newline, no timestamps, and app-root-relative POSIX paths in comments.
Two checkouts at different paths produce identical bytes.

## Adding a field type

`descriptor.d.ts` derivation is a table in
`packages/devtools/src/codegen/type_map.ts`, keyed by the field class's
`static type` (the protocol §8 registry key — a field's `TValue` is a
compile-time-only parameter the worker cannot observe). A new field type plugs
in by adding one row:

```ts
export const FIELD_TS_TYPES = {
  // …
  'vendor/map-point': () => '{ lat: number; lng: number }',
}
```

Rows are functions of the extracted field shape, so props-sensitive types work
— that is how `select` renders a literal union from its declared `options()`.
Unknown keys (plugin fields, types the table has not caught up with) fall back
to `unknown`: the widest safe type, never `any`.

## Failure mode

Any of the following fails the hook with `E_ADONIA_CODEGEN`, naming the
offending **file path**:

- a resource or panel module that throws during evaluation;
- a resource class without a non-empty `static slug`;
- a resource class without a `static model` (it could not be queried, and
  indexing it would only defer the failure to the first request);
- two resources claiming the same slug;
- an evaluation worker that crashes and fails to respawn (the codegen contract).

No partial registry is ever written. During HMR regens the same failure is
logged through the assembler's CLI UI instead and the previous artifacts stay
in place, so a transient syntax error mid-edit never kills the dev server.

## How evaluation reads your code

The worker imports resource and panel modules with a cache-busting `?v=` query
and reads them **structurally** — it never imports `@adonia/core` (core depends
on devtools, and a pnpm peer-variant split would break `instanceof` against
your app's instance anyway). The contract it relies on is documented on the
shapes in `packages/devtools/src/codegen/protocol.ts`:

- resource class: `static slug`, `static model`, optional `schema(builder, ctx)`
  and `table(ctx)`;
- schema node: `key`/`attribute`/`props` for fields (plus the class statics
  `type` and `columnType`), `children` for layouts;
- panel: `id`, and optionally `mountPath`, `domainPattern`, `guardName`,
  `resourceRegistry` (only its keys are read — the lazy imports are never
  invoked).

`schema()` receives a stub layout builder that accepts any factory name, and a
stub `HttpContext` (§14). Codegen does **not** apply `canSee` projection: a
`.d.ts` describes every request, so the emitted types are the union over
requests rather than one request's projection.

Resource modules are not required to be side-effect free (the codegen contract), but their
side effects run inside the worker on every regen — avoid opening connections
or timers at import time.

Source: https://adonia.pages.dev/codegen/index.mdx
