@nocobase/app-template-default 1.0.0-beta.65 → 1.0.0-beta.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/rules/build-and-deploy.md +45 -0
- package/.agents/rules/components.md +34 -0
- package/.agents/rules/shell.md +29 -0
- package/.prettierignore +3 -1
- package/AGENTS.md +43 -367
- package/CLAUDE.md +0 -8
- package/package.json +8 -7
- package/tests/logic/agents-md.test.ts +108 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Building and deploying this application
|
|
2
|
+
|
|
3
|
+
Read this before changing the build, packing a deployment archive, building or editing the Docker image, setting `CDN_BASE_URL`, or building for another platform. The deployment process itself is in the [deployment Skill](../skills/nocobase-deployment/SKILL.md), and dependency placement and the build failure table are in [where to declare a dependency](../skills/nocobase-app-development/references/dependencies.md).
|
|
4
|
+
|
|
5
|
+
## CDN assets at build time
|
|
6
|
+
|
|
7
|
+
`CDN_BASE_URL` sets Vite's `base` only during a production build: `CDN_BASE_URL=https://cdn.example.com/my-app/v1/ pnpm build`. Use a full HTTPS URL ending in `/`; unset, empty or whitespace-only values keep the default `./`. `pnpm build` reads it from the process environment, then `.env.local`, then `.env`; development ignores it and continues using `APP_BASE_PATH`. It is a build parameter, absent from `config variables` and `dist/variables.json`, with no `config.yml` setting. Changing it requires rebuilding.
|
|
8
|
+
|
|
9
|
+
Upload the static files from `dist/client/` to that CDN prefix with their directory structure preserved, and enable CORS for the application's origin. Keep `index.html` and the complete client tree on the application server so it can inject runtime configuration into HTML and serve files still addressed there. Use `resolveAssetUrl('/assets/logo.png')` from `@nocobase/app-client` for a file shipped as `dist/client/assets/logo.png`; template logos use this helper. It reads Vite's build-time `BASE_URL`, uses an absolute HTTP(S) CDN base when configured, and otherwise falls back to the runtime mount path. Keep API paths, page links and runtime-generated files on `resolveAppUrl`. Vite does not rewrite arbitrary URL strings. The README's CDN section describes deployment and Docker usage.
|
|
10
|
+
|
|
11
|
+
For a Docker source build, pass `--build-arg CDN_BASE_URL=https://cdn.example.com/my-app/v1/`. With `DIST=prebuilt`, set `CDN_BASE_URL` when building `dist/` beforehand. Neither a prebuilt image's build argument nor `docker run -e CDN_BASE_URL=...` can change compiled asset URLs.
|
|
12
|
+
|
|
13
|
+
## Packing the build for a deployment
|
|
14
|
+
|
|
15
|
+
`pnpm build --tar` writes `storage/exports/dist.tar.gz` after the build. The archive holds `dist/` as a directory next to `config.example.yml`, so extracting it produces exactly those two paths rather than scattering `server/` and `node_modules/` into whatever directory you unpacked in.
|
|
16
|
+
|
|
17
|
+
`config.example.yml` travels with it because a deployment has to write a `config.yml` before it can start — `pnpm nocobase config init`, run inside `dist/`, does that from the example, in place, and `pnpm nocobase config check` there verifies it, database connection included, before the first start — and the example is the only statement of what may go in it. Directories of executable shims are left out: a `.bin` entry points at a path on the machine that installed it, and a dangling one makes `pnpm install` in the extracted tree report a corrupt store rather than repair it.
|
|
18
|
+
|
|
19
|
+
Without `--tar` no archive is produced, which is what you want when the build is only going to be run locally.
|
|
20
|
+
|
|
21
|
+
## Building a Docker image
|
|
22
|
+
|
|
23
|
+
`Dockerfile` and `Dockerfile.dockerignore` build this application from its sources: a build stage runs `pnpm install --frozen-lockfile` and `pnpm build`, and a `node:24-bookworm-slim` runtime stage receives `dist/` and `config.example.yml` only. The runtime has no pnpm, so it runs the two `dist/package.json` scripts directly: `start` is `node ./server/standalone.js`, which the image starts as `node dist/server/standalone.js`, and `nocobase` is `node ./cli/index.js`, run as `node dist/cli/index.js <topic> <command>`. The deployment root is `/app`, so configuration is `/app/config.yml` and storage is `/app/storage`, both mounted at runtime.
|
|
24
|
+
|
|
25
|
+
The client is not tied to a mount path, so `APP_BASE_PATH` is a runtime variable, `/main` in the image unless the container is started with `-e APP_BASE_PATH=<path>`; the health check probes the same path. The build stage runs on `$BUILDPLATFORM` and passes `--target linux-$TARGETARCH`, so a multi-platform build does not compile under emulation; do not move the build stage to the target platform or switch the runtime to Alpine, which would need the musl target. Keep `Dockerfile.dockerignore` beside the Dockerfile: BuildKit reads it only there, and without it `config.yml`, `.env`, and `storage/` enter the build context.
|
|
26
|
+
|
|
27
|
+
`--build-arg DIST=prebuilt` skips the build stage and packages the `dist/` in the context instead, selected through `FROM dist-${DIST}` so BuildKit never runs the stage it does not use. That stage refuses a `dist/` whose `nocobase.buildTarget` is not linux, glibc, the target architecture and the image's Node major, and one from an `@nocobase/app-cli` that does not record `nocobase.relocatable`, whose client is tied to the path it was built for. The ignore file re-includes `dist/` last and whole, so dependency directories inside it named like excluded paths (`langchain/storage`) survive, and then excludes `dist/.env`, which can carry `DB_PASSWORD` from local `.env` files. Keep those rules in that order.
|
|
28
|
+
|
|
29
|
+
## Building for another platform
|
|
30
|
+
|
|
31
|
+
`pnpm build --help` (or `-h`) lists build options and exits without loading build dependencies, running hooks, or modifying `dist/`. Every successful build records `nocobase.buildTarget` in `dist/package.json`, including builds with no native modules: `platform`, `arch`, `libc`, `nodeMajor`, and `nodeAbi`. Use `libc` only for Linux; its value on other platforms is a compatibility placeholder. Deployment checks should compare these fields with the host runtime and also respect `engines.node`. With `--target current` (the default), the Node version and ABI come from the running process; an explicit platform target defaults to Node 24 unless `--node-version` is supplied.
|
|
32
|
+
|
|
33
|
+
`pnpm build` targets the machine it runs on, so `pnpm build && pnpm start` works. A deployment build says where it is going: `--target linux-x64`, `--target linux-arm64`, `--target linux-x64-musl`, plus `--node-version` when the server's Node major differs. Every build prints the platform it produced and records it in `dist/package.json` under `nocobase.buildTarget`.
|
|
34
|
+
|
|
35
|
+
A `.node` binary must match the target platform, architecture, and C library; addons using the Node ABI must also match its version. `better-sqlite3` 13 uses N-API and bundles its platform binaries, so cross-platform builds retain the target's binary, including the separate `linuxmusl` build for Alpine. `pg`, `mysql2`, and `tedious` are plain JavaScript, so an application using only those is portable as built.
|
|
36
|
+
|
|
37
|
+
## Compiled migration and seed manifests
|
|
38
|
+
|
|
39
|
+
The application build generates `.manifest.json` in each compiled migrations and seeds directory after server compilation, path rewriting, and `afterServerBuild` hooks. Keep the manifest generator in the build when customizing it. Plugins generate their own manifests when built; an application must not regenerate manifests for installed dependencies.
|
|
40
|
+
|
|
41
|
+
TypeScript and compiled JavaScript use the same source checksum for migration history, while the loader separately verifies emitted JavaScript. Marked JavaScript requires its manifest. For a database with old raw JavaScript checksums, first run the compiled representation with matching original output; verified legacy hashes are converted under the task lock. Unreproducible old output remains an error. Never edit historical migrations, and never edit the history table by hand, to resolve an upgrade failure; `pnpm nocobase db repair` is the supported way to realign a checksum you can account for, and `pnpm nocobase db redo` the way to re-run a migration whose branch is still unmerged.
|
|
42
|
+
|
|
43
|
+
## Server start log
|
|
44
|
+
|
|
45
|
+
`pnpm dev` owns the ready banner and public URL; `APP_SERVER_START_LOG=false` suppresses the underlying listener announcement through the `env` of `server/config/server.ts`. Keep that mapping when editing deployment environment settings. Request starts, request headers and config diagnostics use DEBUG; the normal INFO output contains completion summaries. See the shared application development Skill for hosted logging and upgrade limits.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Components and styling
|
|
2
|
+
|
|
3
|
+
Read this before adding or changing a component, a shadcn primitive, a UI Library item, or the application's visual design.
|
|
4
|
+
|
|
5
|
+
## Imports
|
|
6
|
+
|
|
7
|
+
Use package-local imports such as `#components/ui/button` and `#extensions/nocobase-auth-forms/form-parts`. `package.json#imports` declares their targets, and `components.json` uses the same prefixes for shadcn generation. Keep directory entry points explicitly mapped to their index files. Do not add a Vite alias or TypeScript `paths` mapping for these imports.
|
|
8
|
+
|
|
9
|
+
## shadcn/ui primitives
|
|
10
|
+
|
|
11
|
+
Use shadcn/ui for UI. The template ships only the primitives its shell and compositions use: `button`, `dialog`, `dropdown-menu`, `input`, `label`, `popover`, `spinner`, `toast` and `tooltip`. Check `client/components/ui/` first; if the primitive is not there, add it from the shadcn registry rather than writing your own. `yes n |` answers "no" when the CLI offers to overwrite a primitive that is already installed; without an answer, a non-interactive run stops at that question. Format the files it created and translate the English a few primitives carry, as [the shadcn guide](../skills/nocobase-app-development/references/frontend/references/shadcn.md) lists; otherwise keep them as the CLI writes them:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
yes n | pnpm exec shadcn add card
|
|
15
|
+
pnpm exec prettier --write client/components/ui/card.tsx
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Build your own components by composing these primitives, and put them in `client/components/`. A page below another one — a covering child page, a record's own page — leaves by the header's breadcrumb when its route declares `breadcrumb` or the page declares its trail with `usePageBreadcrumb`, and otherwise by `BackButton` (`client/components/back-button.tsx`) above its heading; never both.
|
|
19
|
+
|
|
20
|
+
**Look components up in the shadcn skill.** The development Skill carries the shadcn/ui skill, unchanged from shadcn 4.21.0, in `.agents/skills/nocobase-app-development/references/frontend/shadcn/`. Read [the shadcn guide](../skills/nocobase-app-development/references/frontend/references/shadcn.md) first: it routes to the skill's rules and states where this application departs from them, among others `pnpm exec shadcn` instead of `npx shadcn@latest`, no `shadcn apply` or `--preset`, and create, edit and detail as `RouteDialog` or `RouteDrawer` child routes rather than a `Sheet`. `pnpm exec shadcn docs <name>` gives one primitive's documentation and examples for the Base UI version. For a whole page, start from the worked example that [the example index](../skills/nocobase-app-development/references/frontend/references/example.md) indexes.
|
|
21
|
+
|
|
22
|
+
## UI Library items
|
|
23
|
+
|
|
24
|
+
`PageContainer`, `PageHeader`, `RouteDialog`, `RouteDrawer`, `RouteChildPage` and `useRouteOverlay` in `client/components/` come from the NocoBase UI Library, as do the authentication blocks in `client/extensions/nocobase-auth-forms/`, `nocobase-auth-methods/`, `nocobase-auth-split-layout/` and `nocobase-device-approval/` that `client/pages/auth/` composes, and the inbox block in `client/extensions/nocobase-inbox/` that `client/pages/inbox.tsx` renders on `/inbox`; like the rest of the source, they belong to the application. `yes n | pnpm exec shadcn add @nocobase/<item>` adds another item from the library: a single component lands in `client/components/`, a complete feature in `client/extensions/nocobase-<item>/`. A date field and a list's `DataTable` are the application's own components: compose `DatePicker` in `client/components/date-picker.tsx` from the `calendar` and `popover` primitives, following shadcn's Date Picker guide, and build `DataTable` in `client/components/` from the `table` primitive and `@tanstack/react-table`, following shadcn's Data Table guide. The CLI adds the packages an item or primitive needs to `dependencies`: move the new ones, such as `react-day-picker` for `calendar`, to `devDependencies`, as [where to declare a dependency](../skills/nocobase-app-development/references/dependencies.md) explains for everything client code imports, but leave `@nocobase/i18n` in `dependencies`, where the server needs it, and put back the `^` of its range, which the CLI pins.
|
|
25
|
+
|
|
26
|
+
## Styling and visual consistency
|
|
27
|
+
|
|
28
|
+
Style with the semantic Tailwind tokens — `bg-background`, `text-foreground`, `text-muted-foreground`, `border-border`, `bg-primary` — so pages follow the light and dark themes. Do not hard-code colors like `bg-white` or `text-gray-900`; they break the moment someone switches theme.
|
|
29
|
+
|
|
30
|
+
**Visual consistency is a whole-application property.** Match the surrounding code's spacing, typography, and component choices. If a change genuinely calls for a different look, change the application's design tokens in `client/theme/themes/*.css` so every page moves together. Never restyle only the part you are working on — a page that looks different from the rest is a defect, not a customization.
|
|
31
|
+
|
|
32
|
+
The default brand palette and density live in `client/theme/themes/*.css`; shared hover/cursor and Badge geometry live in `client/styles.css`, outside generated shadcn primitives. For semantic status labels, use `StatusBadge` from `client/components/status-badge.tsx` with `neutral`, `info`, `warning`, or `success`; choose the tone by meaning and keep the visible status text. Reuse these shared components and tokens in new pages so template upgrades can carry the design consistently.
|
|
33
|
+
|
|
34
|
+
**Follow the UI guidelines.** [The UI guidelines](../skills/nocobase-app-development/references/frontend/ui-guidelines.md) set the conventions every page follows, among them the page header and the single placement of the primary action, empty states, row actions behind one "…" menu, dates picked from a `Calendar` in a `Popover` rather than a native date input, and medium-width drawers; the frontend workflow says how much of it to read for a change.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Application shell
|
|
2
|
+
|
|
3
|
+
Read this before changing the layouts, header, sidebar, breadcrumbs, account menu or loading indicators under `client/layouts/`, `client/routing/` and `client/components/`.
|
|
4
|
+
|
|
5
|
+
## Breadcrumbs and layouts
|
|
6
|
+
|
|
7
|
+
Layouts own breadcrumb route context and render the current page's trail in the header after the sidebar toggle (`client/components/breadcrumbs.tsx`): the route trail by default, or the trail a page declares with `usePageBreadcrumb` from `@nocobase/app-client` when it names records. Pages do not render breadcrumbs themselves. `AppRouter` selects routes and layouts. See [pages and routes](../skills/nocobase-app-development/references/frontend/references/page.md) for each layout's scope.
|
|
8
|
+
|
|
9
|
+
## Header and sidebar
|
|
10
|
+
|
|
11
|
+
`client/layouts/components/layout-header.tsx` is a presentation container accepting children. The sidebar is shadcn's Sidebar: `components/app-sidebar.tsx` wraps its provider, desktop icon mode and phone sheet, and `components/navigation-menu.tsx` draws the permission-filtered route tree with `SidebarMenu*` and controlled `Collapsible` sub-menus. Leave `client/components/ui/sidebar.tsx` as the shadcn CLI writes it and customize from outside: widths go through the provider's `style` in spacing units, the phone sheet is rendered in `app-sidebar.tsx` with a translated title, and the provider's Ctrl/Cmd+B shortcut is stopped at `document`. The App layout owns menus, branding, permissions and redirects. Desktop icon mode shows leaf labels as zero-delay tooltips and opens groups in hover and keyboard-focus popovers, preserving filtered navigation, parent-page links, nested groups and the selected group's highlight. Keep this behavior aligned across layouts. Desktop collapse state is shared through `useSidebarPreference` at `nocobase:sidebar:collapsed` across applications on the same origin; whether the phone sheet is open stays local to each layout.
|
|
12
|
+
|
|
13
|
+
The header carries no Settings or Inbox entry. An application has no separate settings area: a page that configures something is an ordinary route in `client/routes.ts` with `navigation`, placed in the sidebar like any other page, for example under a Settings navigation group. The `/inbox` page declares no navigation; give it an entry where the application wants one.
|
|
14
|
+
|
|
15
|
+
Navigation groups retain their expanded or collapsed state while the navigation tree stays mounted. Selecting a new page expands its ancestor groups without collapsing other groups; users can still collapse the active group manually.
|
|
16
|
+
|
|
17
|
+
## Account menu and session
|
|
18
|
+
|
|
19
|
+
The account menu checks Better Auth sign-out results before refreshing the session and shows a localized error toast for API or network failures. Preserve this behavior when upgrading the shell; navigation alone does not revoke a session.
|
|
20
|
+
|
|
21
|
+
The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Pending checks hide protected content, and failed checks deny access.
|
|
22
|
+
|
|
23
|
+
The account menu language control in `client/layouts/components/language-switcher.tsx` uses a shadcn submenu with radio items. Render it inside `DropdownMenuContent` to preserve menu keyboard navigation and selection semantics.
|
|
24
|
+
|
|
25
|
+
## Branded loading
|
|
26
|
+
|
|
27
|
+
Use `Loading` for page or panel waits and `Spinner` (the shadcn-compatible adapter to `BrandSpinner`) for inline pending actions, uploads, and refresh indicators. Both share the stable NocoBase mark and orbit animation; do not introduce independent Lucide spinners for application loading. Keep skeletons for content placeholders and progress bars for determinate progress. `index.html` has the matching pre-JavaScript mark and must stay visually consistent. All animations respect reduced motion.
|
|
28
|
+
|
|
29
|
+
The shared list of shell behaviors to keep when customizing is in [customizing the shell](../skills/nocobase-app-development/references/frontend/references/shell.md).
|
package/.prettierignore
CHANGED
|
@@ -22,5 +22,7 @@ database/*/seeds/
|
|
|
22
22
|
# `.claude/skills/` holds symbolic links into it. They are upstream prose, replaced wholesale on the next sync, so
|
|
23
23
|
# formatting them rewrites files this application does not own and the change disappears anyway. `.gitignore` lists
|
|
24
24
|
# both, but a `--ignore-path` run replaces `.gitignore` rather than adding to it, so they are named here as well.
|
|
25
|
-
|
|
25
|
+
# `.agents/rules/` is the application's own guidance and is formatted like any other document.
|
|
26
|
+
.agents/*
|
|
27
|
+
!.agents/rules/
|
|
26
28
|
.claude/skills/
|
package/AGENTS.md
CHANGED
|
@@ -12,27 +12,21 @@ Default is the clean application starting point. It registers product capabiliti
|
|
|
12
12
|
|
|
13
13
|
`database/main/` contains required permission initialization only; application-owned business migrations and seeds start empty. Do not add article history, demo seeds, or compatibility copies from Examples to this template. Existing installations retain their own executed migration sources when upgrading; see the [upgrade migration rules](.agents/skills/nocobase-app-upgrade/references/edge-cases.md#migrations).
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Keeping this file small
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
This file stays at most 32 KB and must not grow without bound, because every agent loads it on every start. It holds only the rules that apply everywhere and an index of where the detail lives.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
- Application-specific guidance — including what a change to this application's structure, shell or conventions needs the next agent to know — goes in `.agents/rules/<topic>.md`. Update the relevant file, or add one, and add or update its entry in the [index](#index) below rather than writing the detail here.
|
|
20
|
+
- Framework-wide guidance lives in the synchronized Skills under `.agents/skills/`. Link to it, never copy it here; when it is wrong for every application, correct it upstream in `@nocobase/app-skills`.
|
|
21
|
+
- `tests/logic/agents-md.test.ts` enforces the size limit and checks that the links here and in `.agents/rules/` resolve.
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
| Add an HTTP endpoint | `references/server-routes.md` |
|
|
25
|
-
| Name an endpoint, shape its response or errors, validate it | `references/http-api.md` |
|
|
26
|
-
| Read or write data | `references/database-and-data.md` |
|
|
27
|
-
| Change the schema | `references/migrations.md` |
|
|
28
|
-
| Switch or add a database connection | `references/database-connections.md` |
|
|
29
|
-
| Translate server-produced text or add a language | `references/i18n.md` |
|
|
30
|
-
| Add a service, background job, or scheduled task | `references/services-and-jobs.md` |
|
|
31
|
-
| Write tests and verify | `references/testing.md` |
|
|
23
|
+
## Load the development skills
|
|
24
|
+
|
|
25
|
+
`pnpm install` runs `pnpm nocobase skills sync` through the application's `postinstall` hook. If `.agents/skills/nocobase-app-development/` is missing, install dependencies from the application root; if install scripts were disabled or synchronized Skills are stale, run `pnpm nocobase skills sync` explicitly.
|
|
32
26
|
|
|
33
|
-
Read the one page your task needs, not the whole directory.
|
|
27
|
+
[`.agents/skills/nocobase-app-development/SKILL.md`](.agents/skills/nocobase-app-development/SKILL.md) holds the detailed guidance behind this file. Read it first — it routes to the reference that matches your task. Read the one page your task needs, not the whole directory.
|
|
34
28
|
|
|
35
|
-
`.agents/skills/nocobase-app-upgrade/` is a separate Skill for a separate job: merging a newer release of the template this application was generated from. Read it when the task is upgrading the template rather than building a feature, and read it before touching anything — an upgrade done by copying the newest template over this application destroys the work that made it this application.
|
|
29
|
+
`.agents/skills/nocobase-app-upgrade/` is a separate Skill for a separate job: merging a newer release of the template this application was generated from. Read it when the task is upgrading the template rather than building a feature — including when the user asks to upgrade NocoBase or its core version, because a newer template release is how this application moves to a newer NocoBase — and read it before touching anything — an upgrade done by copying the newest template over this application destroys the work that made it this application.
|
|
36
30
|
|
|
37
31
|
`.agents/skills/nocobase-deployment/` covers moving this application from source to a production server: building for the target platform, what the archive does and does not carry, migrations and business data, production configuration, and what to verify afterwards. Read it before building for deployment, and when an application starts in production but does not work.
|
|
38
32
|
|
|
@@ -57,389 +51,71 @@ cli/commands/ Commands this application owns
|
|
|
57
51
|
tests/ Tests; never beside the source
|
|
58
52
|
```
|
|
59
53
|
|
|
60
|
-
A
|
|
61
|
-
|
|
62
|
-
A feature with a page and an API touches five places: a migration for the table, a route in `server/routes/`, a page in `client/pages/` declared in `client/routes.ts`, navigation on the page route, and strings in `client/locales/`.
|
|
63
|
-
|
|
64
|
-
`client/runtime.ts` composes the browser application; `client/react-providers.ts` declares React providers in outer-to-inner layers `root`, `application`, and `extension`. Applications use the first two and plugins own the last; `before` and `after` order providers only within their layer.
|
|
65
|
-
|
|
66
|
-
`server/runtime.ts` composes configuration, plugins, providers and routes; `server/app.ts` assembles the application. `server/standalone.ts` starts the Node listener and `server/embedded.ts` lets a host mount the same runtime. Register endpoints through `server/routes/index.ts`; queue consumers and background jobs are registered from a service provider, not discovered from a directory. Editable module defaults live in `server/config/` and are collected by `defaultAppConfigs` in its `index.ts`; `server/config.ts` loads the configuration file. Each section declares the environment variables that set it in `env` of its `defineAppConfig`, with a `description` and, where it applies, `secret`, `required`, `generate` or `firstStartOnly` in the helper's second argument; `pnpm nocobase config env` lists them all, `pnpm nocobase config variables` describes each, and `pnpm build` writes that description to `dist/variables.json` for whoever deploys the build. `server/config/database.ts` maps `DB_*` onto the main connection and `server/config/users.ts` maps `INITIAL_ADMIN_*` onto the initial administrator. `config.yml` is read as written: a `${NAME}` in it is not expanded.
|
|
67
|
-
|
|
68
|
-
### The rest is framework structure
|
|
54
|
+
A feature with a page and an API touches five places: a migration for the table, a route in `server/routes/`, a page in `client/pages/` declared in `client/routes.ts`, navigation on the page route, and strings in `client/locales/`. A page with children or page-local helpers uses a folder with `index.tsx`; child folders mirror route paths. [Application structure](.agents/skills/nocobase-app-development/references/app-structure.md) explains how the runtime composes these pieces, where configuration and its environment variables are declared, and the runtime paths.
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`client/layouts/components/layout-header.tsx` is a presentation container accepting children. The sidebar is shadcn's Sidebar: `components/app-sidebar.tsx` wraps its provider, desktop icon mode and phone sheet, and `components/navigation-menu.tsx` draws the permission-filtered route tree with `SidebarMenu*` and controlled `Collapsible` sub-menus. Leave `client/components/ui/sidebar.tsx` as the shadcn CLI writes it and customize from outside: widths go through the provider's `style` in spacing units, the phone sheet is rendered in `app-sidebar.tsx` with a translated title, and the provider's Ctrl/Cmd+B shortcut is stopped at `document`. The App layout owns menus, branding, permissions and redirects. Desktop icon mode shows leaf labels as zero-delay tooltips and opens groups in hover and keyboard-focus popovers, preserving filtered navigation, parent-page links, nested groups and the selected group's highlight. Keep this behavior aligned across layouts. Desktop collapse state is shared through `useSidebarPreference` at `nocobase:sidebar:collapsed` across applications on the same origin; whether the phone sheet is open stays local to each layout.
|
|
73
|
-
|
|
74
|
-
The header carries no Settings or Inbox entry. An application has no separate settings area: a page that configures something is an ordinary route in `client/routes.ts` with `navigation`, placed in the sidebar like any other page, for example under a Settings navigation group. The `/inbox` page declares no navigation; give it an entry where the application wants one.
|
|
75
|
-
|
|
76
|
-
`client/routing/`, `client/layouts/`, `client/theme/`, the server entry points, the `package.json` scripts that call `nocobase`, and the tsconfigs are the scaffolding the template provides. It is still this application's own source — it shipped to the user and they may change it — but it is the part the template evolves, so an edit there is what a future upgrade has to reconcile.
|
|
77
|
-
|
|
78
|
-
Prefer the mechanism the system already provides. Declare a page in `client/routes.ts` and add `navigation` when it needs a menu entry. Refine resources add no menu entries and, with no data provider registered, load nothing; pages load data through `useApiClient`. A page uses `authz` to restrict access; a plugin page is customized through an option or an override. Before editing the shell to add a menu, check the route and its `navigation` declaration.
|
|
79
|
-
|
|
80
|
-
When the built-in mechanism genuinely cannot express what is being asked, changing this structure is a legitimate answer — not a last resort to apologize for. Do it deliberately, and leave the next agent enough to work with:
|
|
81
|
-
|
|
82
|
-
- Comment what you changed and why the built-in path did not fit. On upgrade, an agent reconciling the template's version needs to know whether your change is still needed.
|
|
83
|
-
- Update this file in the same change, so the guidance describes the application as it actually is. If the framework's shared guidance is wrong for every application, update the source `@nocobase/app-skills` package instead of editing its synchronized copy.
|
|
84
|
-
|
|
85
|
-
## Building a feature
|
|
56
|
+
## Rules that apply everywhere
|
|
86
57
|
|
|
87
58
|
### Pages and routes
|
|
88
59
|
|
|
89
|
-
Declare
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
const appRoutes: AppClientRouteContribution = defineAppRoutes([
|
|
93
|
-
{
|
|
94
|
-
name: 'orders',
|
|
95
|
-
path: '/orders',
|
|
96
|
-
auth: 'required',
|
|
97
|
-
authz: { resource: { type: 'page', id: 'orders' }, action: 'access' },
|
|
98
|
-
componentLoader: () => import('./pages/orders.js'),
|
|
99
|
-
},
|
|
100
|
-
]);
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Route paths are application-internal. Never write the deployment base path such as `/main` into one — the runtime restores it.
|
|
104
|
-
|
|
105
|
-
`auth` controls browser navigation only: `required` for signed-in pages, `guest` for sign-in and registration, `optional` for pages that work either way. It is not server security. An endpoint the page calls enforces its own authentication independently.
|
|
106
|
-
|
|
107
|
-
Declare every page with `defineAppRoutes()`; there is no `/settings` or `/dev` surface. An administrative page is an ordinary route with `navigation` and `authz`, enforced by the server, and plugins contribute no settings pages for the application to host.
|
|
108
|
-
|
|
109
|
-
**Declare navigation on the route.** Menus read `navigation: { title: 'navigation.orders' }`; titles resolve in the owning locale namespace. Add the translation in `client/locales/`. Refine resources do not add menu entries.
|
|
110
|
-
|
|
111
|
-
Use recursive groups to organize menus; their path is optional. Pages may also have children, but must manually render `Outlet`. For URL-addressable dialogs and drawers, declare the child in `defineAppRoutes()` in `client/routes.ts`, place the owning page's `Outlet`, then render `RouteDialog` or `RouteDrawer`. A child page of its own — a record's page, a form too long for a dialog — returns `RouteChildPage` around its `PageContainer`; only tab content renders inline, and a child route that returns a bare `PageContainer` renders below the parent's content instead of covering it. Call `useRouteOverlay()` only from a descendant rendered inside the overlay, including its footer, never from the page returning the wrapper. A record opens over the page the user is on: a dashboard or a board that shows records declares their drawer under its own route instead of linking to the list's, and a row menu's Edit opens its own dialog rather than the one stacked on the drawer. On a page with tabs the user is on the tab being shown: an overlay the page's header opens is declared under every tab and linked through the current tab (`` `${tab}/edit` ``, never a bare `edit`, which resolves beside the tabs), so it stacks on that tab and closing returns to it. Read `.agents/skills/nocobase-app-development/references/frontend/references/overlay.md` before implementing overlays or close guards.
|
|
112
|
-
|
|
113
|
-
Declare `authz` on the first page of every path: `{ resource: { type: 'page', id: 'orders' }, action: 'access' }` for a product page, `'unrestricted'` for a page only root may open, or `'skip'`. A nested page that omits it inherits its nearest ancestor page's value, and a child's own value overrides it. A first page that omits it still registers, with a development warning: protected pages become unrestricted-only, which hides them from everyone but root, while guest and optional pages skip the check. Always declare it rather than relying on that default. Parent guards still apply when a child skips. Menus and loaders read the declared rule; endpoints enforce authorization independently. The page id identifies stored page grants, so changing it requires migrating the grants that reference it.
|
|
60
|
+
Declare every page in `client/routes.ts` with `defineAppRoutes()` and keep its component behind a lazy `componentLoader` whose module default-exports it. Never write the deployment base path such as `/main` into a route — the runtime restores it. `auth` controls browser navigation only and is not server security. Declare `navigation` on the route to put it in a menu, and always declare `authz` on the first page of every path; the page id identifies stored page grants, so changing it requires migrating the grants that reference it. Read [pages and routes](.agents/skills/nocobase-app-development/references/frontend/references/page.md), and [overlays](.agents/skills/nocobase-app-development/references/frontend/references/overlay.md) before implementing dialogs, drawers or close guards.
|
|
114
61
|
|
|
115
62
|
### Components and styling
|
|
116
63
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Use shadcn/ui for UI. The template ships only the primitives its shell and compositions use: `button`, `dialog`, `dropdown-menu`, `input`, `label`, `popover`, `spinner`, `toast` and `tooltip`. Check `client/components/ui/` first; if the primitive is not there, add it from the shadcn registry rather than writing your own. `yes n |` answers "no" when the CLI offers to overwrite a primitive that is already installed; without an answer, a non-interactive run stops at that question. Format the files it created and translate the English a few primitives carry, as `.agents/skills/nocobase-app-development/references/frontend/references/shadcn.md` lists; otherwise keep them as the CLI writes them:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
yes n | pnpm exec shadcn add card
|
|
123
|
-
pnpm exec prettier --write client/components/ui/card.tsx
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Build your own components by composing these primitives, and put them in `client/components/`. A page below another one — a covering child page, a record's own page — leaves by the header's breadcrumb when its route declares `breadcrumb` or the page declares its trail with `usePageBreadcrumb`, and otherwise by `BackButton` (`client/components/back-button.tsx`) above its heading; never both.
|
|
127
|
-
|
|
128
|
-
`PageContainer`, `PageHeader`, `RouteDialog`, `RouteDrawer`, `RouteChildPage` and `useRouteOverlay` in `client/components/` come from the NocoBase UI Library, as do the authentication blocks in `client/extensions/nocobase-auth-forms/`, `nocobase-auth-methods/`, `nocobase-auth-split-layout/` and `nocobase-device-approval/` that `client/pages/auth/` composes, and the inbox block in `client/extensions/nocobase-inbox/` that `client/pages/inbox.tsx` renders on `/inbox`; like the rest of the source, they belong to the application. `yes n | pnpm exec shadcn add @nocobase/<item>` adds another item from the library: a single component lands in `client/components/`, a complete feature in `client/extensions/nocobase-<item>/`. A date field and a list's `DataTable` are the application's own components: compose `DatePicker` in `client/components/date-picker.tsx` from the `calendar` and `popover` primitives, following shadcn's Date Picker guide, and build `DataTable` in `client/components/` from the `table` primitive and `@tanstack/react-table`, following shadcn's Data Table guide. The CLI adds the packages an item or primitive needs to `dependencies`: move the new ones, such as `react-day-picker` for `calendar`, to `devDependencies`, as "Adding a dependency" below explains for everything client code imports, but leave `@nocobase/i18n` in `dependencies`, where the server needs it, and put back the `^` of its range, which the CLI pins.
|
|
129
|
-
|
|
130
|
-
**Look components up in the shadcn skill.** The development Skill carries the shadcn/ui skill, unchanged from shadcn 4.21.0, in `.agents/skills/nocobase-app-development/references/frontend/shadcn/`. Read `references/frontend/references/shadcn.md` first: it routes to the skill's rules and states where this application departs from them, among others `pnpm exec shadcn` instead of `npx shadcn@latest`, no `shadcn apply` or `--preset`, and create, edit and detail as `RouteDialog` or `RouteDrawer` child routes rather than a `Sheet`. `pnpm exec shadcn docs <name>` gives one primitive's documentation and examples for the Base UI version. For a whole page, start from the worked example that `references/frontend/references/example.md` indexes.
|
|
131
|
-
|
|
132
|
-
Style with the semantic Tailwind tokens — `bg-background`, `text-foreground`, `text-muted-foreground`, `border-border`, `bg-primary` — so pages follow the light and dark themes. Do not hard-code colors like `bg-white` or `text-gray-900`; they break the moment someone switches theme.
|
|
133
|
-
|
|
134
|
-
**Visual consistency is a whole-application property.** Match the surrounding code's spacing, typography, and component choices. If a change genuinely calls for a different look, change the application's design tokens in `client/theme/themes/*.css` so every page moves together. Never restyle only the part you are working on — a page that looks different from the rest is a defect, not a customization.
|
|
64
|
+
Import with the package-local `#components/...` and `#extensions/...` specifiers; never add a Vite alias or TypeScript `paths` mapping for them. Use shadcn/ui: check `client/components/ui/` first and add a missing primitive with `yes n | pnpm exec shadcn add <name>` rather than writing your own. Style only with the semantic Tailwind tokens, never hard-coded colors, and keep visual consistency application-wide: change the design tokens in `client/theme/themes/*.css` rather than restyling one page. This template's components, primitives and styling details are in [.agents/rules/components.md](.agents/rules/components.md).
|
|
135
65
|
|
|
136
|
-
|
|
66
|
+
### Server routes and the API document
|
|
137
67
|
|
|
138
|
-
|
|
68
|
+
Every `/api` route follows [HTTP API design](.agents/skills/nocobase-app-development/references/http-api.md): camelCase paths, `{ data }` on success, `ApiError` on failure, input validated through `apiValidator()`, and a `describeRoute()` declaration; never write an error body by hand. **Every route owns its own security**: mounting under `/api` authenticates nothing, so install `auth.required()` and an explicit authorization check on the paths the route owns. Scope middleware to those paths, never `router.use('*', ...)`. A public webhook still verifies a signature, timestamp, or one-time state. Keep domain logic in a service under `server/providers/`. See [server routes](.agents/skills/nocobase-app-development/references/server-routes.md).
|
|
139
69
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`defineApiRoutes()` mounts under `/api`, and `defineRootRoutes()` mounts at the root for callbacks and webhooks. Do not repeat `/api` in the path. Each route factory creates and returns its own Hono router.
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
export const apiRoutes: AppApiRouteContribution<Application> = defineApiRoutes(
|
|
146
|
-
(app) => {
|
|
147
|
-
const router = new Hono();
|
|
148
|
-
const auth = app.container.resolve(authenticationToken);
|
|
149
|
-
|
|
150
|
-
router.use('/orders', auth.required());
|
|
151
|
-
router.get(
|
|
152
|
-
'/orders',
|
|
153
|
-
describeRoute({
|
|
154
|
-
tags: ['Orders'],
|
|
155
|
-
summary: 'List orders',
|
|
156
|
-
operationId: 'listOrders',
|
|
157
|
-
responses: {
|
|
158
|
-
'200': listResponse(OrderSchema),
|
|
159
|
-
// A session but no permission check; the query validator adds the 400.
|
|
160
|
-
'401': apiErrorResponse(401),
|
|
161
|
-
'500': apiErrorResponse(500),
|
|
162
|
-
},
|
|
163
|
-
}),
|
|
164
|
-
apiValidator('query', ListOrdersQuery),
|
|
165
|
-
async (context) =>
|
|
166
|
-
context.json(await listOrders(context.req.valid('query'))),
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
return router;
|
|
170
|
-
},
|
|
171
|
-
);
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
**Every `/api` route follows the HTTP API design** in `.agents/skills/nocobase-app-development/references/http-api.md`: camelCase paths, `{ data }` on success, `throw new ApiError(...)` from `@nocobase/app-server/router` on failure, input validated with zod through `apiValidator()`, and a `describeRoute()` declaration for the API document, all from `@nocobase/app-server/router`. Never write an error body by hand.
|
|
175
|
-
|
|
176
|
-
**Every route owns its own security.** Mounting under `/api` does not authenticate anything. Install `auth.required()` on the paths the route owns, and add `authorization.middleware()` with an explicit `resource`/`action` check when the operation needs permission rather than just identity. Never rely on middleware from another route or on the order routes happen to be registered in.
|
|
177
|
-
|
|
178
|
-
Scope middleware to the exact paths you own, or to an isolated sub-router mounted at your prefix. A `router.use('*', ...)` leaks into contributions mounted later.
|
|
179
|
-
|
|
180
|
-
A webhook that a third party calls cannot use a login session, so it is deliberately public — but public still means verifying a signature, timestamp, or one-time state, and testing that anonymous requests without a valid signature are rejected.
|
|
181
|
-
|
|
182
|
-
Keep HTTP concerns in the route and domain logic in a service under `server/providers/`. Services do not read Hono contexts, return HTTP status codes, or decide retry behavior.
|
|
183
|
-
|
|
184
|
-
Bind services to their existing tokens in a provider's `register()`; calling `createServiceToken` twice with the same name creates different keys. Do not connect to databases, start workers, or execute route factories at module top level. Acquire long-lived resources in `start()` and release them in `shutdown()`. Providers and routes read typed configuration rather than `process.env`.
|
|
185
|
-
|
|
186
|
-
### API documentation
|
|
187
|
-
|
|
188
|
-
The running application serves an OpenAPI 3.1 document of every `/api` route it has — its own, every registered plugin's, the data endpoints and Better Auth's `/api/auth/...` — at `<origin><APP_BASE_PATH>/api/swagger` (JSON), with Swagger UI at `<origin><APP_BASE_PATH>/api/swagger/docs`. Locally that is `http://127.0.0.1:13000/main/api/swagger/docs`, with `/main` replaced by the actual `APP_BASE_PATH`.
|
|
189
|
-
|
|
190
|
-
Reading either needs a signed-in session or an API key: open the Swagger UI in a browser where you are signed in, or send a key in `x-api-key`. A signed-in user creates keys with `POST <APP_BASE_PATH>/api/apiKeys`; the `nocobase-app-plugin-api-keys` Skill covers the plugin. Without a valid credential the routes answer `401` with reason `API_DOCS_UNAUTHENTICATED`, and an application with no access check registered, such as one without the authentication plugin, answers `404`.
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
curl -H "x-api-key: <key>" http://127.0.0.1:13000/main/api/swagger
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
**Learn the available endpoints from this document instead of reading route sources.** Before calling an endpoint or building on one, fetch the JSON with a key the user gives you and find the operations by `tags` or `operationId`; read the source only for what the document does not say. Fetch it again after registering a plugin or changing a Collection.
|
|
197
|
-
|
|
198
|
-
Every `/api` route the application writes declares itself, as in the example above: `tags`, an English `summary` and a unique camelCase `operationId`; input through `apiValidator()`; responses through `dataResponse()`, `listResponse()`, `emptyResponse()` and `apiErrorResponse(status)` for exactly the statuses the route can produce. Do not list `400` for input validation: a route that uses `apiValidator` gets the `400` automatically. List `400` yourself only for another reason, such as a failed precondition. `apiErrorResponses` is `401`, `403` and `500`, for an authenticated route with a permission check; otherwise list each status the route can return with `apiErrorResponse(code)`. Response schemas live in `server/routes/schemas.ts`, typed against what the service returns. A route reached without a credential adds `security: []`. Hide a route with `describeRoute({ hide: true })` and a comment only for the few reasons `references/http-api.md` lists. Data endpoints are documented without a declaration; a field a data exposure adds to every record goes in its `computedFields`. A test starting the application expects `findUndeclaredApiRoutes()` and `findApiDocumentSchemaProblems()` to be empty; `.agents/skills/nocobase-app-development/references/testing.md` shows it.
|
|
70
|
+
**Learn the available endpoints from the API document instead of reading route sources.** The running application serves it at `<APP_BASE_PATH>/api/swagger`, readable with a signed-in session or an `x-api-key` the user gives you; ask the user for a key rather than creating one, and fetch it again after registering a plugin or changing a Collection. See [the API document](.agents/skills/nocobase-app-development/references/http-api.md#api-documentation).
|
|
199
71
|
|
|
200
72
|
### Database
|
|
201
73
|
|
|
202
|
-
Schema changes are migrations under `database/main/migrations
|
|
203
|
-
|
|
204
|
-
Declare database defaults with `export default defineAppDatabaseConfig((runtime) => ({ connections }))`. Before provider registration, the runtime asynchronously imports configured official drivers; explicit `drivers` registrations override them. Keep `isolatedDeclarations: false` for application server declarations so configuration and connection fields retain inference. See `.agents/skills/nocobase-app-development/references/database-connections.md`.
|
|
205
|
-
|
|
206
|
-
`database/<connection>/collections/` holds what the database currently resolves each Collection to — `collection.json`, `metadata.json` and `schema.json` per Collection plus a `_manifest.json` — written by `pnpm nocobase collections generate`, which `db apply`, `db redo`, `db rollback`, `db reset` and `pnpm dev` run for every connection whose migrations they change. Every file there is derived, for every connection: the directory is gitignored, safe to delete, and never read back. Edit metadata through migrations or the Collection Metadata Service and regenerate, never by hand, and never import these files from a migration. An `external` connection, if you add one, takes its hand-written metadata from `database/<connection>/metadata/<name>.json` instead, which is committed. `pnpm nocobase collections generate --check` fails when they are out of date.
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
const migration: MigrationDefinition = defineMigration({
|
|
210
|
-
name: '202609020001_create_orders',
|
|
211
|
-
async up({ builder }) {
|
|
212
|
-
await builder.createCollection('orders', (collection) => {
|
|
213
|
-
collection.increments('id');
|
|
214
|
-
collection.string('reference', { length: 64, nullable: false });
|
|
215
|
-
collection.datetime('createdAt', { nullable: false });
|
|
216
|
-
});
|
|
217
|
-
},
|
|
218
|
-
async down({ builder }) {
|
|
219
|
-
await builder.dropCollection('orders');
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**A migration is immutable history and must be self-contained.** Spell out every field, index, and constraint in the migration itself. Never import a collection definition, model, or registry that keeps evolving — doing so silently changes what an already-applied migration means. Write `down` as the explicit reverse in a safe dependency order.
|
|
225
|
-
|
|
226
|
-
Edit an existing migration only while the branch that introduced it is unmerged. Once merged, every correction is a new migration. Editing one that has already run changes nothing on its own: it is recorded as executed, so `pnpm nocobase db apply` skips it. Run `pnpm nocobase db redo` to roll the latest batch back and apply it again from the corrected source. Editing it also makes its recorded checksum stop matching; a run reports that as a warning and keeps going, `onChecksumMismatch: 'error'` makes it refuse, and `pnpm nocobase db repair` realigns the history for a change that leaves the schema identical — a reformat or a comment — never for one the database has not received. `pnpm nocobase db reset` starts over from an empty schema when the batch cannot be rolled back, and takes every row with it.
|
|
227
|
-
|
|
228
|
-
The exported `name` must match the filename. Apply with `pnpm nocobase db apply` and verify against a real database.
|
|
229
|
-
|
|
230
|
-
At runtime, resolve `databaseManagerToken` from the container and use `database.query()` to read and write the default connection. Use `database.query('analytics')` for another connection. Application tasks use `database/<connectionName>/{migrations,seeds}` and bind to that connection explicitly; plugin tasks and default runtime access stay on `database.default`. Only managed connections run migrations or seeds. See the migrations reference for execution and upgrade rules.
|
|
74
|
+
Schema changes are migrations under `database/main/migrations/`; required data is a seed under `database/main/seeds/`. **A migration is immutable history and must be self-contained**: spell out every field, index and constraint, never import an evolving definition, and write `down` as the explicit reverse. Edit an existing migration only while the branch that introduced it is unmerged; once merged, every correction is a new migration. The exported `name` must match the filename. Never edit `database/<connection>/collections/`; it is regenerated. Read [migrations and seeds](.agents/skills/nocobase-app-development/references/migrations.md), [database and data access](.agents/skills/nocobase-app-development/references/database-and-data.md) and [database connections](.agents/skills/nocobase-app-development/references/database-connections.md).
|
|
231
75
|
|
|
232
76
|
### User-facing text
|
|
233
77
|
|
|
234
|
-
Every string a user reads goes through a translation key.
|
|
235
|
-
|
|
236
|
-
```tsx
|
|
237
|
-
const { t } = useTranslation();
|
|
238
|
-
t('orders.title');
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
To reword a plugin's string, add an `overrides` block keyed by that plugin's package name in your locale file. Do not edit the plugin.
|
|
242
|
-
|
|
243
|
-
The languages the application offers are its own locale files, not a configured list, and the two sides are read separately: `client/locales/` decides what the picker shows, while `server/locales/` decides which languages the server can answer in. Prefer adding a language to both when server-produced text needs translating, but a client-only language is valid: the interface switches normally and the server falls back to English with an informational notice. `pnpm nocobase locales check` reports one declared on a single side and exits nonzero until the lists align; that check does not block the runtime switch.
|
|
244
|
-
|
|
245
|
-
The account menu language control in `client/layouts/components/language-switcher.tsx` uses a shadcn submenu with radio items. Render it inside `DropdownMenuContent` to preserve menu keyboard navigation and selection semantics.
|
|
246
|
-
|
|
247
|
-
## Application access URLs
|
|
248
|
-
|
|
249
|
-
When reporting or opening this application's address, use the `Local:` URL printed by `pnpm dev`, including the application base path. In normal full-stack development, the Hono server proxies Vite: the user-facing entry uses the server port (`APP_SERVER_PORT`, default 13000), while Vite's port (normally 5173) serves the frontend development process. A response from Vite alone does not verify the application entry.
|
|
250
|
-
|
|
251
|
-
For an existing run without available startup output, identify its process and working directory, confirm the actual server listening port and configured base path, and check that URL before sharing it. Occupied ports advance automatically, so do not assume defaults. Report when the run belongs to another checkout. Use a host reachable by the user; `localhost` refers to the machine where the browser runs.
|
|
252
|
-
|
|
253
|
-
With `PROXY_TARGET_URL`, the local Hono server is skipped and Vite becomes the local application entry. Use the printed `Local:` URL in this mode too; `APP_SERVER_PORT` selects its local port (default 5173). See the remote backend development guidance below.
|
|
254
|
-
|
|
255
|
-
## Development file watching
|
|
256
|
-
|
|
257
|
-
Changes to `.env` and `.env.local` (including creation, atomic replacement, and deletion) restart the full development run after its previous processes exit. The new run reloads environment files, ports, proxy settings, and startup hooks; explicit shell variables still take precedence. Use the newly printed URL if the port or base path changes. This also applies in proxy mode. `config.yml` changes restart only the local server. `NOCOBASE_STRICT_STARTUP=true` disables both automatic restarts.
|
|
258
|
-
|
|
259
|
-
Changes to `package.json`, the lockfile, and the package manager's install state restart the local server once they have been quiet for a few seconds. An install writes several of them over as long as fetching and linking take, so restarting on the first write would bring the server back against a half-installed `node_modules` — and the next write would arrive while it was still shutting down, which is where the watcher escalates to SIGKILL. Waiting for quiet turns one install into one restart.
|
|
78
|
+
Every string a user reads goes through a translation key. Write the application's copy in `client/locales/en-US.ts` and `zh-CN.ts`, never in `client/locales/system/`, which a template upgrade replaces; extend a shared group by spreading it first. To reword a plugin's string, add an `overrides` block keyed by that plugin's package name in your locale file; do not edit the plugin. See [frontend copy](.agents/skills/nocobase-app-development/references/frontend/references/i18n.md) and [internationalization](.agents/skills/nocobase-app-development/references/i18n.md).
|
|
260
79
|
|
|
261
|
-
|
|
80
|
+
### The command line
|
|
262
81
|
|
|
263
|
-
`pnpm
|
|
82
|
+
`pnpm nocobase <topic> <command>` runs this application's CLI; only `pnpm dev`, `pnpm build`, `pnpm start` and the quality scripts are scripts. Read [the command line](.agents/skills/nocobase-app-development/references/cli.md) before running a command: `db reset`, `db rollback`, `db redo` and `db repair` refuse to run without `--force` outside a terminal, and that refusal is not something to work around. Commands this application owns live in `cli/commands/`; see [adding an application command](.agents/skills/nocobase-app-development/references/commands.md).
|
|
264
83
|
|
|
265
|
-
|
|
84
|
+
### Dependencies
|
|
266
85
|
|
|
267
|
-
|
|
86
|
+
Put a package your **server** code (`server/`, `database/`, `cli/`) imports in `dependencies`, and everything your **client** code imports — along with build tooling, tests, and type-only imports — in `devDependencies`. Keep `@nocobase/db` in `dependencies` alongside the database driver. Remove a direct `@nocobase/*` dependency only with `pnpm nocobase package remove` after reviewing its usage. See [where to declare a dependency](.agents/skills/nocobase-app-development/references/dependencies.md).
|
|
268
87
|
|
|
269
|
-
|
|
88
|
+
### Plugins
|
|
270
89
|
|
|
271
|
-
|
|
90
|
+
**Read a plugin's Skill before building what it already does.** Building a permission system, a notification sender, or a job scheduler by hand when a registered plugin already provides one is the most expensive mistake available here. Register and update plugins with `pnpm nocobase plugin register` and `pnpm nocobase plugin update`; customize a plugin's page through its options, a source extension or `client/route-overrides.ts`, never by redeclaring its route. `.agents/skills/` is generated output; never edit a file there. See [plugins](.agents/skills/nocobase-app-development/references/plugins.md).
|
|
272
91
|
|
|
273
|
-
|
|
92
|
+
### Running the application
|
|
274
93
|
|
|
275
|
-
|
|
94
|
+
When reporting or opening this application's address, use the `Local:` URL printed by `pnpm dev`, including the application base path, and never infer it from defaults or Vite's port. See [the development server](.agents/skills/nocobase-app-development/references/dev-server.md) for restarts, file watching and developing against another backend.
|
|
276
95
|
|
|
277
|
-
|
|
96
|
+
### Before you finish
|
|
278
97
|
|
|
279
|
-
|
|
98
|
+
Scope all verification — formatting, lint, type checking, tests, builds and runtime checks — to the affected files, projects, or packages and their affected consumers. Do not run full-application or workspace-wide checks, or an aggregate `pnpm check`, as a routine step after each edit. Documentation-only changes need formatting and link checks for the changed documents, not type checking, runtime tests, or builds. Report which checks ran, their scope, and any unverified behavior.
|
|
280
99
|
|
|
281
|
-
|
|
100
|
+
Add tests for what you changed — a route's authenticated, unauthenticated, and unauthorized responses; a migration's `up` and `down` against a real database; a page's actual behavior — in `tests/`, never beside the source; browser tests go in `tests/playwright/`. See [testing and verification](.agents/skills/nocobase-app-development/references/testing.md).
|
|
282
101
|
|
|
283
|
-
|
|
102
|
+
For any UI work, start with [the frontend workflow](.agents/skills/nocobase-app-development/references/frontend/ui-workflow.md), which decides the workflow and routes to the styling rules, the theme token contract and the rest.
|
|
284
103
|
|
|
285
|
-
|
|
104
|
+
## Changing the framework structure
|
|
286
105
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
`cli/` is compiled into `dist`, and a deployment runs the same CLI without the development commands as `node dist/cli/index.js <topic> <command>`. Change shared behavior in `@nocobase/app-cli` rather than copying it into the application; use plugin build and dev hooks for local build and development extensions.
|
|
290
|
-
|
|
291
|
-
## Plugins
|
|
292
|
-
|
|
293
|
-
Plugins are registered in `client/plugins.ts`, `server/plugins.ts`, and `cli/plugins.ts`. Presence in the array enables a plugin and array order is contribution order. A plugin appears in the roots matching what it ships, so a plugin with only commands is listed in `cli/plugins.ts` alone. Bulk Skills synchronization and plugin updates discover plugins from these composition roots.
|
|
294
|
-
|
|
295
|
-
Let `pnpm nocobase plugin register` and `pnpm nocobase plugin unregister` add and remove entries. Edit these files by hand only to reorder entries or to pass a plugin its options.
|
|
296
|
-
|
|
297
|
-
Update one registered plugin with `pnpm nocobase plugin update @nocobase/app-plugin-authentication`, or omit the name to update all registered plugins. Prefer the full package name; `authentication` is also accepted as a short name. The name is a positional argument, not `--plugin`. Use `--dry-run` to preview. With pnpm, updates stay within declared version ranges; after a successful update, all registered plugin Skills are re-synchronized. See [Plugins in the README](README.MD#plugins) for examples and update scope.
|
|
298
|
-
|
|
299
|
-
To customize a page a plugin owns, pass an option on its registration, add a source extension under `client/extensions/*/extension.ts`, or add an entry to `client/route-overrides.ts`. Do not redeclare the plugin's route — a duplicate `/install` is a conflict, not a customization. An override replaces only `componentLoader`; route identity, path, and auth mode stay with the plugin. One route takes one override across all three mechanisms. Authentication pages are not plugin-owned: `/login`, `/register`, `/forgot-password`, and `/reset-password` are application routes declared in `client/routes.ts`.
|
|
300
|
-
|
|
301
|
-
Route overrides must stay lazy, declare a `componentEntry`, and load a default-exported component. Authentication pages in `client/pages/auth/` use relative links and the authentication plugin's `client/actions` hooks; do not call authentication endpoints directly or create a second session store.
|
|
302
|
-
|
|
303
|
-
### Read a plugin's Skill before building what it already does
|
|
304
|
-
|
|
305
|
-
**You are not starting from scratch.** This application ships with NocoBase packages that already solve whole categories of requirement, and packages may publish a Skill explaining how to use them. `pnpm nocobase skills sync` copies Skills from direct `@nocobase/*` dependencies and registered plugins into `.agents/skills/`. Before implementing a feature, check whether an installed and registered plugin already covers it:
|
|
306
|
-
|
|
307
|
-
| The requirement sounds like | Read the Skill for |
|
|
308
|
-
| -------------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
309
|
-
| Email, IM, or in-app messages; notifying someone that something happened | `@nocobase/app-plugin-notification` |
|
|
310
|
-
| Roles, permissions, "user A may only see their own records", field-level or row-level access | `@nocobase/app-plugin-authorization` |
|
|
311
|
-
| Sign-in, registration, sessions, password reset | `@nocobase/app-plugin-authentication` |
|
|
312
|
-
| User listing, account state, password reset, and application-owned role assignment | `@nocobase/app-plugin-users` |
|
|
313
|
-
| File upload and metadata through Repository | `@nocobase/app-plugin-file` |
|
|
314
|
-
| Translated text and language switching | `@nocobase/app-plugin-i18n` |
|
|
315
|
-
|
|
316
|
-
Run `pnpm nocobase skills sync` if `.agents/skills/` is missing or looks out of date, then read the Skill for the plugin you need. It documents that plugin's public entries, the ownership boundary, and how to verify the result — which is faster and more correct than inferring an API from its source.
|
|
317
|
-
|
|
318
|
-
Building a permission system, a notification sender, or a job scheduler by hand when a registered plugin already provides one is the most expensive mistake available here. Prefer the plugin; write your own only when you have read its Skill and confirmed it genuinely does not fit.
|
|
319
|
-
|
|
320
|
-
`.agents/skills/` is generated output: gitignored, and every synchronized package-owned directory is replaced wholesale on the next sync, so never edit a file there. The same sync mirrors each synchronized directory into `.claude/skills/` as a symbolic link, because Claude Code discovers skills only there; that mirror is generated and gitignored too. Put application-specific guidance in committed `AGENTS.md` files.
|
|
321
|
-
|
|
322
|
-
## Removing a NocoBase dependency
|
|
323
|
-
|
|
324
|
-
Before removing a direct `@nocobase/*` dependency, search application imports, Client/Server/CLI plugin registrations, routes, services, configuration, tests, and build scripts for its package name and public contracts. Migrate or delete those references first. The command below changes dependency metadata and generated Skills; it does not edit application source or configuration, so never use it to remove a capability the application still needs.
|
|
325
|
-
|
|
326
|
-
Use `pnpm nocobase package remove @nocobase/example`. It invokes the application's package manager so `package.json` and the lockfile stay consistent, then deletes only synchronized Skills recorded as belonging to that package. For an `@nocobase/app-plugin-*` package it delegates to the plugin unregister command, removing its Client, Server, and CLI registrations together; `pnpm nocobase plugin unregister <name>` remains a supported plugin-specific entry point. Preview with `--dry-run`, and use `--json` when another tool needs structured output.
|
|
327
|
-
|
|
328
|
-
After an interrupted or manual removal, first confirm the manifest no longer declares the package, then run a full `pnpm nocobase skills sync` to reconcile stale package-owned output. `package remove` may also be passed a package already absent from the manifest to clean recorded historical Skill ownership, and it must not uninstall or clean Skills owned by another package.
|
|
329
|
-
|
|
330
|
-
## Adding a dependency
|
|
331
|
-
|
|
332
|
-
Keep `@nocobase/db` in `dependencies` alongside the database driver. It supplies the driver's runtime peer and lets TypeScript resolve the inferred database configuration declaration through the public package name. Moving it to `devDependencies` can cause TS2883 in an installed application even while the source workspace builds successfully.
|
|
333
|
-
|
|
334
|
-
Put a package your **server** code imports in `dependencies`. Put everything your **client** code imports — along with build tooling, tests, and type-only imports — in `devDependencies`.
|
|
335
|
-
|
|
336
|
-
That split looks backwards until you see how the two halves are deployed. `pnpm build` bundles the client: Vite resolves every client import and inlines it into `dist/client`, so nothing has to resolve it again later. The server is not bundled. `dist/server` keeps its bare imports, and `pnpm build` generates `dist/package.json` from your `dependencies` and installs a `node_modules` next to it — that tree is what the deployed server resolves against, and `devDependencies` are not in it.
|
|
337
|
-
|
|
338
|
-
So the two mistakes fail in opposite ways. A server import left in `devDependencies` works all through development and fails only on the server, with a bare `Cannot find package` naming nothing that points back here. A client package put in `dependencies` never breaks anything — it is just installed into every deployment, where the server never requires it. That one is invisible, so it accumulates: `lucide-react` and `@xyflow/react` were 44 MB of it before this rule was written down.
|
|
339
|
-
|
|
340
|
-
What decides it is where the importing file lives and what the import is, not what the package is for. `import ts from 'typescript'` in `server/` is a runtime dependency even though TypeScript sounds like tooling. `import type { Config } from 'x'` is erased before anything runs, so it stays a devDependency wherever it appears. A dynamic `import()` counts — deferring the load changes when a package is needed, not whether.
|
|
341
|
-
|
|
342
|
-
### Adding a dependency
|
|
343
|
-
|
|
344
|
-
Which half of the application imports it decides where it goes.
|
|
345
|
-
|
|
346
|
-
| The import is reached from | Declare it in |
|
|
347
|
-
| --------------------------------------- | ----------------- |
|
|
348
|
-
| `server/`, `database/`, or `cli/` | `dependencies` |
|
|
349
|
-
| `client/`, build tooling, tests | `devDependencies` |
|
|
350
|
-
| `import type` only, wherever it appears | `devDependencies` |
|
|
351
|
-
|
|
352
|
-
`dist/package.json` is generated from `dependencies` and is what a deployment installs from, so a server import declared as a devDependency resolves in every development checkout and is absent exactly once — on the deployed server. A client import needs nothing at runtime: Vite resolves and inlines it into `dist/client` at build time.
|
|
353
|
-
|
|
354
|
-
A plugin's browser packages arrive by a third route and need nothing from you. Plugins declare those as peer dependencies, so installing a plugin brings one shared copy into this application, while `dist/` sets `autoInstallPeers: false` and installs none of them.
|
|
355
|
-
|
|
356
|
-
`pnpm build` checks the server half: it reads every value import in `server/`, `database/`, and `cli/` and fails the build if any of those packages is missing from `dist/package.json`. It cannot see an import whose specifier is built at run time:
|
|
357
|
-
|
|
358
|
-
```ts
|
|
359
|
-
await import(`${name}/index.js`); // invisible to the check
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Declare such a package in `dependencies` when you write the code; nothing will remind you later.
|
|
363
|
-
|
|
364
|
-
### CDN assets at build time
|
|
365
|
-
|
|
366
|
-
`CDN_BASE_URL` sets Vite's `base` only during a production build: `CDN_BASE_URL=https://cdn.example.com/my-app/v1/ pnpm build`. Use a full HTTPS URL ending in `/`; unset, empty or whitespace-only values keep the default `./`. `pnpm build` reads it from the process environment, then `.env.local`, then `.env`; development ignores it and continues using `APP_BASE_PATH`. It is a build parameter, absent from `config variables` and `dist/variables.json`, with no `config.yml` setting. Changing it requires rebuilding.
|
|
367
|
-
|
|
368
|
-
Upload the static files from `dist/client/` to that CDN prefix with their directory structure preserved, and enable CORS for the application's origin. Keep `index.html` and the complete client tree on the application server so it can inject runtime configuration into HTML and serve files still addressed there. Use `resolveAssetUrl('/assets/logo.png')` from `@nocobase/app-client` for a file shipped as `dist/client/assets/logo.png`; template logos use this helper. It reads Vite's build-time `BASE_URL`, uses an absolute HTTP(S) CDN base when configured, and otherwise falls back to the runtime mount path. Keep API paths, page links and runtime-generated files on `resolveAppUrl`. Vite does not rewrite arbitrary URL strings. The README's CDN section describes deployment and Docker usage.
|
|
369
|
-
|
|
370
|
-
For a Docker source build, pass `--build-arg CDN_BASE_URL=https://cdn.example.com/my-app/v1/`. With `DIST=prebuilt`, set `CDN_BASE_URL` when building `dist/` beforehand. Neither a prebuilt image's build argument nor `docker run -e CDN_BASE_URL=...` can change compiled asset URLs.
|
|
371
|
-
|
|
372
|
-
### Packing the build for a deployment
|
|
373
|
-
|
|
374
|
-
`pnpm build --tar` writes `storage/exports/dist.tar.gz` after the build. The archive holds `dist/` as a directory next to `config.example.yml`, so extracting it produces exactly those two paths rather than scattering `server/` and `node_modules/` into whatever directory you unpacked in.
|
|
375
|
-
|
|
376
|
-
`config.example.yml` travels with it because a deployment has to write a `config.yml` before it can start — `pnpm nocobase config init`, run inside `dist/`, does that from the example, in place, and `pnpm nocobase config check` there verifies it, database connection included, before the first start — and the example is the only statement of what may go in it. Directories of executable shims are left out: a `.bin` entry points at a path on the machine that installed it, and a dangling one makes `pnpm install` in the extracted tree report a corrupt store rather than repair it.
|
|
377
|
-
|
|
378
|
-
Without `--tar` no archive is produced, which is what you want when the build is only going to be run locally.
|
|
379
|
-
|
|
380
|
-
### Building a Docker image
|
|
381
|
-
|
|
382
|
-
`Dockerfile` and `Dockerfile.dockerignore` build this application from its sources: a build stage runs `pnpm install --frozen-lockfile` and `pnpm build`, and a `node:24-bookworm-slim` runtime stage receives `dist/` and `config.example.yml` only. The runtime has no pnpm, so it runs the two `dist/package.json` scripts directly: `start` is `node ./server/standalone.js`, which the image starts as `node dist/server/standalone.js`, and `nocobase` is `node ./cli/index.js`, run as `node dist/cli/index.js <topic> <command>`. The deployment root is `/app`, so configuration is `/app/config.yml` and storage is `/app/storage`, both mounted at runtime.
|
|
383
|
-
|
|
384
|
-
The client is not tied to a mount path, so `APP_BASE_PATH` is a runtime variable, `/main` in the image unless the container is started with `-e APP_BASE_PATH=<path>`; the health check probes the same path. The build stage runs on `$BUILDPLATFORM` and passes `--target linux-$TARGETARCH`, so a multi-platform build does not compile under emulation; do not move the build stage to the target platform or switch the runtime to Alpine, which would need the musl target. Keep `Dockerfile.dockerignore` beside the Dockerfile: BuildKit reads it only there, and without it `config.yml`, `.env`, and `storage/` enter the build context.
|
|
385
|
-
|
|
386
|
-
`--build-arg DIST=prebuilt` skips the build stage and packages the `dist/` in the context instead, selected through `FROM dist-${DIST}` so BuildKit never runs the stage it does not use. That stage refuses a `dist/` whose `nocobase.buildTarget` is not linux, glibc, the target architecture and the image's Node major, and one from an `@nocobase/app-cli` that does not record `nocobase.relocatable`, whose client is tied to the path it was built for. The ignore file re-includes `dist/` last and whole, so dependency directories inside it named like excluded paths (`langchain/storage`) survive, and then excludes `dist/.env`, which can carry `DB_PASSWORD` from local `.env` files. Keep those rules in that order.
|
|
387
|
-
|
|
388
|
-
### Building for another platform
|
|
389
|
-
|
|
390
|
-
`pnpm build --help` (or `-h`) lists build options and exits without loading build dependencies, running hooks, or modifying `dist/`. Every successful build records `nocobase.buildTarget` in `dist/package.json`, including builds with no native modules: `platform`, `arch`, `libc`, `nodeMajor`, and `nodeAbi`. Use `libc` only for Linux; its value on other platforms is a compatibility placeholder. Deployment checks should compare these fields with the host runtime and also respect `engines.node`. With `--target current` (the default), the Node version and ABI come from the running process; an explicit platform target defaults to Node 24 unless `--node-version` is supplied.
|
|
391
|
-
|
|
392
|
-
`pnpm build` targets the machine it runs on, so `pnpm build && pnpm start` works. A deployment build says where it is going: `--target linux-x64`, `--target linux-arm64`, `--target linux-x64-musl`, plus `--node-version` when the server's Node major differs. Every build prints the platform it produced and records it in `dist/package.json` under `nocobase.buildTarget`.
|
|
393
|
-
|
|
394
|
-
A `.node` binary must match the target platform, architecture, and C library; addons using the Node ABI must also match its version. `better-sqlite3` 13 uses N-API and bundles its platform binaries, so cross-platform builds retain the target's binary, including the separate `linuxmusl` build for Alpine. `pg`, `mysql2`, and `tedious` are plain JavaScript, so an application using only those is portable as built.
|
|
395
|
-
|
|
396
|
-
### When a build or a deployment fails
|
|
397
|
-
|
|
398
|
-
| Symptom | Cause | Fix |
|
|
399
|
-
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
400
|
-
| `Cannot find module 'x'` on the server, works locally | `x` is in `devDependencies`, which `dist/package.json` is not generated from | Move it to `dependencies` |
|
|
401
|
-
| A browser package will not resolve while building the application | A plugin declares it as a peer and nothing provides it | Add it to the application's `devDependencies` |
|
|
402
|
-
| `Error loading shared library`, `invalid ELF header`, or a bare `.node` path at startup | The binary does not match the server | Compare `require('./dist/package.json').nocobase.buildTarget` with the server's `process.platform`, `process.arch`, and `process.versions.modules`, then rebuild with matching flags |
|
|
403
|
-
| `no prebuilt binary for <target>` during the build | The package publishes no build for that combination | Check the package supports the target; musl coverage is thinner than glibc |
|
|
404
|
-
|
|
405
|
-
Node ABI to major version: 115 is Node 20, 127 is 22, 137 is 24, 147 is 26.
|
|
406
|
-
|
|
407
|
-
## Before you finish
|
|
408
|
-
|
|
409
|
-
For each change, scope all verification to the affected files, projects, or packages and their affected consumers. This applies to formatting, lint, type checking, tests, builds, and runtime verification. Run only the checks relevant to the change: use explicit file paths for formatting, lint, and tests; use the owning project's TypeScript configuration for type checking; build only affected packages or supported build targets. In a workspace, use `pnpm --filter <affected-package> <script>`. In a standalone application, use its supported file or project selectors; do not invent flags or bypass project configuration to force a narrower check.
|
|
410
|
-
|
|
411
|
-
Do not run full-application or workspace-wide checks, or an aggregate `pnpm check`, as a routine step after each edit. If a necessary check cannot be narrowed further, run the smallest supported project or package scope and explain why. Expand scope only when shared code, dependencies, configuration, or a failure gives a concrete reason, or when the user explicitly requests it. After checks pass, repeat them only for further relevant changes or unresolved failures. Documentation-only changes need formatting and link checks for the changed documents, not type checking, runtime tests, or builds.
|
|
412
|
-
|
|
413
|
-
Report which checks ran, their scope, and any unverified behavior. See the application development Skill's `references/testing.md` for selection examples.
|
|
414
|
-
|
|
415
|
-
Add tests for what you changed: a route's authenticated, unauthenticated, and unauthorized responses; a migration's `up` and `down` against a real database; a page's actual behavior. Tests belong in `tests/`; browser tests go in `tests/playwright/`, which Playwright runs against a running application at `APP_URL` and Vitest skips. Never place a test beside the source it covers.
|
|
416
|
-
|
|
417
|
-
For any UI work, start with `.agents/skills/nocobase-app-development/references/frontend/ui-workflow.md` (from the application root), which decides the workflow and routes to the rest. Its styling rules are in `references/styling.md` beside it: which semantic Tailwind utilities to use so components respond to theme changes, and how to keep deliberate fixed-size exceptions explicit. The full color, font, size, spacing, radius and shadow token contract, and creating or editing theme presets, are in `references/theme.md`.
|
|
418
|
-
|
|
419
|
-
Application startup defaults belong in `config.yml`: `i18n.defaultLocale` for the language, and `client.app.defaultColorScheme` and `client.app.defaultTheme` for appearance. Valid browser-local choices take precedence. Which languages the application offers is not configured — its own `client/locales/` and `server/locales/` are that list. See the i18n and themes references.
|
|
420
|
-
|
|
421
|
-
## Compiled migration and seed manifests
|
|
422
|
-
|
|
423
|
-
The application build generates `.manifest.json` in each compiled migrations and seeds directory after server compilation, path rewriting, and `afterServerBuild` hooks. Keep the manifest generator in the build when customizing it. Plugins generate their own manifests when built; an application must not regenerate manifests for installed dependencies.
|
|
424
|
-
|
|
425
|
-
TypeScript and compiled JavaScript use the same source checksum for migration history, while the loader separately verifies emitted JavaScript. Marked JavaScript requires its manifest. For a database with old raw JavaScript checksums, first run the compiled representation with matching original output; verified legacy hashes are converted under the task lock. Unreproducible old output remains an error. Never edit historical migrations, and never edit the history table by hand, to resolve an upgrade failure; `pnpm nocobase db repair` is the supported way to realign a checksum you can account for, and `pnpm nocobase db redo` the way to re-run a migration whose branch is still unmerged.
|
|
426
|
-
|
|
427
|
-
The account menu checks Better Auth sign-out results before refreshing the session and shows a localized error toast for API or network failures. Preserve this behavior when upgrading the shell; navigation alone does not revoke a session.
|
|
428
|
-
|
|
429
|
-
The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Pending checks hide protected content, and failed checks deny access.
|
|
430
|
-
|
|
431
|
-
Navigation groups retain their expanded or collapsed state while the navigation tree stays mounted. Selecting a new page expands its ancestor groups without collapsing other groups; users can still collapse the active group manually.
|
|
432
|
-
|
|
433
|
-
## Development logging
|
|
434
|
-
|
|
435
|
-
`pnpm dev` owns the ready banner and public URL; `APP_SERVER_START_LOG=false` suppresses the underlying listener announcement through the `env` of `server/config/server.ts`. Keep that mapping when editing deployment environment settings. Request starts, request headers and config diagnostics use DEBUG; the normal INFO output contains completion summaries. See the shared application development Skill for hosted logging and upgrade limits.
|
|
106
|
+
`client/routing/`, `client/layouts/`, `client/theme/`, the server entry points, the `package.json` scripts that call `nocobase`, and the tsconfigs are the scaffolding the template provides. It is still this application's own source — it shipped to the user and they may change it — but it is the part the template evolves, so an edit there is what a future upgrade has to reconcile.
|
|
436
107
|
|
|
437
|
-
|
|
108
|
+
Prefer the mechanism the system already provides. Declare a page in `client/routes.ts` and add `navigation` when it needs a menu entry. Refine resources add no menu entries and, with no data provider registered, load nothing; pages load data through `useApiClient`. A page uses `authz` to restrict access; a plugin page is customized through an option or an override. Before editing the shell to add a menu, check the route and its `navigation` declaration.
|
|
438
109
|
|
|
439
|
-
|
|
110
|
+
When the built-in mechanism genuinely cannot express what is being asked, changing this structure is a legitimate answer — not a last resort to apologize for. Do it deliberately, and leave the next agent enough to work with:
|
|
440
111
|
|
|
441
|
-
|
|
112
|
+
- Comment what you changed and why the built-in path did not fit. On upgrade, an agent reconciling the template's version needs to know whether your change is still needed.
|
|
113
|
+
- Record what the next agent needs to know in the matching `.agents/rules/` file, or a new one linked from the index below, so the guidance describes the application as it actually is without growing this file. If the framework's shared guidance is wrong for every application, update the source `@nocobase/app-skills` package instead of editing its synchronized copy.
|
|
442
114
|
|
|
443
|
-
##
|
|
115
|
+
## Index
|
|
444
116
|
|
|
445
|
-
|
|
117
|
+
| Read | When |
|
|
118
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
119
|
+
| [.agents/rules/components.md](.agents/rules/components.md) | Adding or changing a component, a shadcn primitive, a UI Library item, or the visual design |
|
|
120
|
+
| [.agents/rules/shell.md](.agents/rules/shell.md) | Changing layouts, the header, sidebar, breadcrumbs, the account menu or loading indicators |
|
|
121
|
+
| [.agents/rules/build-and-deploy.md](.agents/rules/build-and-deploy.md) | Changing the build, packing a deployment, the Docker image, `CDN_BASE_URL`, or building for another platform |
|
package/CLAUDE.md
CHANGED
|
@@ -1,9 +1 @@
|
|
|
1
1
|
@AGENTS.md
|
|
2
|
-
|
|
3
|
-
For any UI work, start with `.agents/skills/nocobase-app-development/references/frontend/ui-workflow.md` (from the application root), which decides the workflow and routes to the rest. Its styling rules are in `references/styling.md` beside it: which semantic Tailwind utilities to use so components respond to theme changes, and how to keep deliberate fixed-size exceptions explicit. The full color, font, size, spacing, radius and shadow token contract, and creating or editing theme presets, are in `references/theme.md`.
|
|
4
|
-
|
|
5
|
-
## Compiled migration and seed manifests
|
|
6
|
-
|
|
7
|
-
The application build generates `.manifest.json` in each compiled migrations and seeds directory after server compilation, path rewriting, and `afterServerBuild` hooks. Keep the manifest generator in the build when customizing it. Plugins generate their own manifests when built; an application must not regenerate manifests for installed dependencies.
|
|
8
|
-
|
|
9
|
-
TypeScript and compiled JavaScript use the same source checksum for migration history, while the loader separately verifies emitted JavaScript. Marked JavaScript requires its manifest. For a database with old raw JavaScript checksums, first run the compiled representation with matching original output; verified legacy hashes are converted under the task lock. Unreproducible old output remains an error. Never edit historical migrations, and never edit the history table by hand, to resolve an upgrade failure; `pnpm nocobase db repair` is the supported way to realign a checksum you can account for, and `pnpm nocobase db redo` the way to re-run a migration whose branch is still unmerged.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.67",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.67"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "2.0.0",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"tar": "^7.5.22",
|
|
78
78
|
"@nocobase/app-client": "^3.0.0-beta.1",
|
|
79
79
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
80
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
80
|
+
"@nocobase/app-skills": "0.1.0-beta.34",
|
|
81
81
|
"@nocobase/app-testing": "0.1.0-beta.3",
|
|
82
82
|
"@nocobase/app-websocket": "^0.0.2-beta.0",
|
|
83
83
|
"@nocobase/dev-config": "0.1.0-beta.22",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"config.example.yml",
|
|
114
114
|
"README.MD",
|
|
115
115
|
"AGENTS.md",
|
|
116
|
+
".agents/rules",
|
|
116
117
|
"CLAUDE.md",
|
|
117
118
|
"playwright.config.ts"
|
|
118
119
|
],
|
|
@@ -142,18 +143,18 @@
|
|
|
142
143
|
"better-auth": "1.7.5",
|
|
143
144
|
"hono": "^4.13.2",
|
|
144
145
|
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.16",
|
|
145
|
-
"@nocobase/app-plugin-authorization": "^1.0.0-beta.27",
|
|
146
146
|
"@nocobase/app-plugin-authentication": "^2.0.0-beta.7",
|
|
147
|
+
"@nocobase/app-plugin-authorization": "^1.0.0-beta.27",
|
|
147
148
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.14",
|
|
148
149
|
"@nocobase/app-plugin-file": "^1.0.0-beta.23",
|
|
149
150
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.16",
|
|
150
|
-
"@nocobase/app-plugin-mail": "^1.0.0-beta.
|
|
151
|
+
"@nocobase/app-plugin-mail": "^1.0.0-beta.13",
|
|
151
152
|
"@nocobase/app-plugin-notification": "^1.0.0-beta.27",
|
|
152
153
|
"@nocobase/app-plugin-notification-in-app": "^1.0.0-beta.24",
|
|
153
154
|
"@nocobase/app-plugin-notification-providers": "^0.2.0-beta.11",
|
|
154
155
|
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.17",
|
|
155
156
|
"@nocobase/app-plugin-users": "^2.0.0-beta.4",
|
|
156
|
-
"@nocobase/app-server": "^2.0.0-beta.
|
|
157
|
+
"@nocobase/app-server": "^2.0.0-beta.5",
|
|
157
158
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
158
159
|
"@nocobase/db": "^1.0.0-beta.19",
|
|
159
160
|
"@nocobase/db-sqlite": "^0.1.0-beta.5",
|
|
@@ -169,7 +170,7 @@
|
|
|
169
170
|
"@nocobase/i18n": "^1.0.0-beta.7",
|
|
170
171
|
"@nocobase/caching": "^0.1.0-beta.3",
|
|
171
172
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
172
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
173
|
+
"@nocobase/app-cli": "^1.0.0-beta.21"
|
|
173
174
|
},
|
|
174
175
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
175
176
|
"imports": {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const appRoot = path.resolve(
|
|
9
|
+
fileURLToPath(new URL('.', import.meta.url)),
|
|
10
|
+
'../..',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Every agent loads `AGENTS.md` on every start, so its size is a cost paid on every task. It used to grow with each
|
|
15
|
+
* change that documented itself there; the detail now lives in `.agents/rules/` and is read on demand through links.
|
|
16
|
+
*/
|
|
17
|
+
const AGENTS_MD_LIMIT_BYTES = 32 * 1024;
|
|
18
|
+
|
|
19
|
+
const rulesDirectory = path.join(appRoot, '.agents', 'rules');
|
|
20
|
+
const skillsDirectory = path.join(appRoot, '.agents', 'skills');
|
|
21
|
+
|
|
22
|
+
function listMarkdownFiles(directory: string): string[] {
|
|
23
|
+
if (!existsSync(directory)) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return readdirSync(directory).flatMap((entry) => {
|
|
27
|
+
const entryPath = path.join(directory, entry);
|
|
28
|
+
if (statSync(entryPath).isDirectory()) {
|
|
29
|
+
return listMarkdownFiles(entryPath);
|
|
30
|
+
}
|
|
31
|
+
return entry.endsWith('.md') ? [entryPath] : [];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Relative link targets of a Markdown file, leaving out fenced code blocks and inline code spans. */
|
|
36
|
+
function readRelativeLinks(file: string): string[] {
|
|
37
|
+
const prose = readFileSync(file, 'utf8')
|
|
38
|
+
.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[^\n]*$/gmu, '')
|
|
39
|
+
.replace(/`[^`\n]*`/gu, '');
|
|
40
|
+
const targets: string[] = [];
|
|
41
|
+
for (const match of prose.matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/gu)) {
|
|
42
|
+
const target = match[1] ?? '';
|
|
43
|
+
if (
|
|
44
|
+
target === '' ||
|
|
45
|
+
target.startsWith('#') ||
|
|
46
|
+
/^[a-z][a-z0-9+.-]*:/iu.test(target)
|
|
47
|
+
) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
targets.push(target.replace(/[#?].*$/u, ''));
|
|
51
|
+
}
|
|
52
|
+
return targets;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isInside(parent: string, child: string): boolean {
|
|
56
|
+
const relative = path.relative(parent, child);
|
|
57
|
+
return (
|
|
58
|
+
relative === '' ||
|
|
59
|
+
(!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const agentsMd = path.join(appRoot, 'AGENTS.md');
|
|
64
|
+
const documents = [agentsMd, ...listMarkdownFiles(rulesDirectory)];
|
|
65
|
+
|
|
66
|
+
describe('AGENTS.md', () => {
|
|
67
|
+
it('stays within the size every agent can afford to load on start', () => {
|
|
68
|
+
const size = statSync(agentsMd).size;
|
|
69
|
+
expect(
|
|
70
|
+
size,
|
|
71
|
+
`AGENTS.md is ${size} bytes, over the ${AGENTS_MD_LIMIT_BYTES}-byte limit. Move the detail into .agents/rules/<topic>.md and link it from the index in AGENTS.md; keep only the rules that apply everywhere in AGENTS.md itself.`,
|
|
72
|
+
).toBeLessThanOrEqual(AGENTS_MD_LIMIT_BYTES);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('links every file in .agents/rules/ from its index', () => {
|
|
76
|
+
const linked = new Set(
|
|
77
|
+
readRelativeLinks(agentsMd).map((target) =>
|
|
78
|
+
path.resolve(appRoot, target),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
const unlinked = listMarkdownFiles(rulesDirectory)
|
|
82
|
+
.filter((file) => !linked.has(file))
|
|
83
|
+
.map((file) => path.relative(appRoot, file));
|
|
84
|
+
expect(
|
|
85
|
+
unlinked,
|
|
86
|
+
'Add each rules file to the index in AGENTS.md, saying when to read it, or an agent will never find it.',
|
|
87
|
+
).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('has no broken links in AGENTS.md or .agents/rules/', () => {
|
|
91
|
+
// `.agents/skills/` is written by `pnpm nocobase skills sync` during install, so its links can only be checked once
|
|
92
|
+
// it exists; every other target is a committed file of this application.
|
|
93
|
+
const checkSkills = existsSync(skillsDirectory);
|
|
94
|
+
const broken = documents.flatMap((file) =>
|
|
95
|
+
readRelativeLinks(file)
|
|
96
|
+
.map((target) => path.resolve(path.dirname(file), target))
|
|
97
|
+
.filter(
|
|
98
|
+
(resolved) => checkSkills || !isInside(skillsDirectory, resolved),
|
|
99
|
+
)
|
|
100
|
+
.filter((resolved) => !existsSync(resolved))
|
|
101
|
+
.map(
|
|
102
|
+
(resolved) =>
|
|
103
|
+
`${path.relative(appRoot, file)} -> ${path.relative(appRoot, resolved)}`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
expect(broken).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
});
|