Skip to content

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.

bash
npm run smart-admin -- generate post

Options ​

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 command

The 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.

json
{
  "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 ​

KeyRequiredWhat it is
rowYesOne 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.
rowsPathYesWhere the array sits in the raw response, dot-separated: data.items for { data: { items: [...] } }. Use "" when the response is the array.
filterFieldsYesThe filter bar, in order. Each entry is { key, type } with type one of text, number, select, date. Write [] to say there are none.
idKeyNoThe field that identifies a row, when it is not id. Used as the table's row key.
annotationsNoEverything the shape cannot say.

Annotations ​

AnnotationWhat it declares
typeOne 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.
optionsThe allowed values. Required for type: "status" and for any select filter; a set of choices cannot be inferred from one row.
nullableThe field can come back null. Generates | null on the type and a — guard in the cell.
sometimesThe field is only in some responses. Generates an optional property.
nestedFor 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.ts

With --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 under post_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:

ts
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:

bash
npm run smart-admin -- generate category --plural categories

The module directory keeps the singular name; the route and label take the plural.