generate
Scaffolds a list module from one sample row of your own API. It reads the shape you give it and infers nothing from a name.
npm run smart-admin -- generate postOptions
Usage: smart-admin generate [options] <name>
Scaffold a module from its block in smart-admin.generate.json
Arguments:
name Module name (singular, e.g. "post")
Options:
--plural <plural> Override the auto-pluralized form (e.g. "categories")
--store Generate a Pinia store skeleton
--no-store Skip the store without being asked
--dry-run Print the plan; write nothing
-h, --help display help for commandThe name is singular and the name is the marker: generate post reads the post block. Without --store or --no-store the command asks, and in a non-interactive shell it says so and skips the store.
The input file
Write your shapes in smart-admin.generate.json. Every top-level key is a module; keys starting with _ are notes and are ignored.
{
"post": {
"rowsPath": "data.items",
"idKey": "id",
"row": {
"id": 41,
"title": "How we cut our build time in half",
"cover": "https://cdn.example.com/covers/41.webp",
"status": "published",
"excerpt": "A short summary shown in listings.",
"views": 1284,
"author": { "id": 7, "name": "Amina Haddad" },
"tags": ["engineering", "performance"],
"published_at": "2026-03-01T09:30:00Z"
},
"annotations": {
"cover": { "type": "image" },
"published_at": { "type": "date" },
"status": { "type": "status", "options": ["draft", "published", "archived"] },
"excerpt": { "nullable": true },
"views": { "sometimes": true },
"author": { "nested": "name" }
},
"filterFields": [
{ "key": "status", "type": "select" },
{ "key": "title", "type": "text" },
{ "key": "published_at", "type": "date" }
]
}
}The keys
| Key | Required | What it is |
|---|---|---|
row | Yes | One representative row from your list endpoint, with real values. Its keys become the columns, the TypeScript interface and the translation keys. A key missing here does not exist as far as the generator is concerned, so paste a row that has every field. |
rowsPath | Yes | Where the array sits in the raw response, dot-separated: data.items for { data: { items: [...] } }. Use "" when the response is the array. |
filterFields | Yes | The filter bar, in order. Each entry is { key, type } with type one of text, number, select, date. Write [] to say there are none. |
idKey | No | The field that identifies a row, when it is not id. Used as the table's row key. |
annotations | No | Everything the shape cannot say. |
Annotations
| Annotation | What it declares |
|---|---|
type | One of text, number, date, datetime, image, status, boolean. Only needed when the JSON type is not the whole story — a date, an image URL and a status are all just strings on the wire. |
options | The allowed values. Required for type: "status" and for any select filter; a set of choices cannot be inferred from one row. |
nullable | The field can come back null. Generates | null on the type and a — guard in the cell. |
sometimes | The field is only in some responses. Generates an optional property. |
nested | For an object field, the sub-key to show in the table — "name" for author.name. The nested interface is generated either way; without this the column is left out, and the command says so. |
Every annotated field has to exist in row, or the command refuses. That is what catches a typo.
What it writes
src/modules/post/
├── index.ts
├── pages/
│ └── index.vue
└── types/
├── post.schemas.ts
└── post.types.tsWith --store, one more file — src/modules/post/stores/posts.store.ts, named from the plural.
And it edits, without being asked:
src/locales/*.json— 16 keys underpost_module, in all nine files. Real words in the base language, the same words as placeholders in the other eight.src/main.ts— the side-effect import.src/layouts/navItems.ts— the menu entry.
Both edits arrive annotated, saying where they came from and what to do with them — the output below quotes each line it added.
The real output
Generating module "post"
from the "post" block in smart-admin.generate.json
route /posts · translations under post_module
Store: no — you passed --no-store
created src/modules/post/index.ts
created src/modules/post/pages/index.vue
created src/modules/post/types/post.schemas.ts
created src/modules/post/types/post.types.ts
edited src/locales/*.json — 9 locale file(s)
16 keys · real words in "en" (the base), the same words
as placeholders in 8 other language(s) — translate them there.
edited src/main.ts
// Added by `smart-admin generate post`.
import '@modules/post'
edited src/layouts/navItems.ts
// Added by `smart-admin generate post`. Move it into whichever
// section it belongs to, and swap `Blocks` for the right icon.
{ label: 'Posts', labelKey: 'post_module.title', to: '/posts', icon: Blocks },
✔ Module "post" generated.
Next:
1. Restart the dev server. unplugin-vue-router only picks up /posts on boot.
2. Fill in the fetch: src/modules/post/pages/index.vue, at "// fetch data will be here".
3. Translate post_module in src/locales/ — every language but the base holds
the base's own words as placeholders.
Not generated, because they cannot be known from here: permissions,
the API call and its auth, the pagination request contract, and what
belongs in a store. Each one is a marked seam, not an omission.The generated type
The sample row above produces this, with nothing inferred from a field's name:
export type PostId = number
export type PostStatus = 'draft' | 'published' | 'archived'
export interface PostAuthor {
id: number
name: string
}
export interface Post extends Record<string, unknown> {
id: PostId
title: string
cover: string
status: PostStatus
excerpt: string | null
views?: number
author: PostAuthor
tags: string[]
published_at: string
}excerpt is nullable and views optional because the annotations said so. status is a union because options listed one. Post extends Record<string, unknown> so it satisfies the DataTable and useCrud constraint.
When it refuses
No block for that name. It names what it did find and prints the shortest block that would work:
smart-admin.generate.json has no "widget" block.
It declares: post
Add one, then run this again. The shortest block that works:
"widget": {
"rowsPath": "data",
"row": { "id": 1, "name": "Example" },
"filterFields": []
}No input file yet, with --dry-run. There is nothing to plan against:
smart-admin.generate.json does not exist yet, and --dry-run will not create it.
Run the same command without --dry-run once. It copies
smart-admin.generate.example.json to smart-admin.generate.json and adds that path to .gitignore,
so your real API shapes stay out of git.It also refuses when an annotation names a field that is not in row, and when a select filter has no options on that field.
--plural
The plural is auto-derived and used for the route, the store file name and the menu label. Override it when English is not on your side:
npm run smart-admin -- generate category --plural categoriesThe module directory keeps the singular name; the route and label take the plural.
Related
generate-form— the form half.- Add a module — the whole flow.
- Schema — the
FieldDefinition[]it writes. - DataTable — the table the page renders.