chs: re-add drifted files

dot_agents/dot_skill-lock.json
This commit is contained in:
Prad Nukala committed 2026-08-14 13:53:55 -04:00
1 parent 03ef5c94ea
commit 42d17e825e
30 files changed
+230 -145

No files matched your search

+2 -2
View File
@@ -44,8 +44,8 @@ The single biggest shift for most sources — especially Mintlify — is that **
- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.
- **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).
- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order.
- **Sidebar render mode is global, not per-folder.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and applies to **every** group at once. (It used to live on each folder's `meta.ts` as `display`; that field was **removed** — writing it in a `meta.ts` is now a build error. Set it once in config instead.) An explicit `navigation.sidebar` item may still override its own group's `display`.
- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order.
- **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page — `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter → `meta.ts` → global config → `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).
- **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).
- **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting you must **either** move those pages into a real subfolder (`meta.ts` for label/`collapsed`) — which changes their URLs, so add `redirects` — **or** declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.
@@ -41,7 +41,7 @@ Every Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png
## Navigation: `sidebars.js` + `_category_.json`
- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.
- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed` (**no `display` field in `meta.ts`** — if collapse matters, set the global `navigation.sidebar.display: "group"` once in config).
- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: "group"` (or `navigation.sidebar.display: "group"` once in config when every category collapses).
- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible`/`className`/`customProps` → drop (report).
- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.
@@ -39,7 +39,7 @@ Everything else is `defineConfig({ title })`.
| `description` | **drop** (folders have no description) |
| `collapsible: false` | **drop** (report — no per-folder equivalent) |
`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages` — there is **no `display` field** (writing one is a build error). Collapse rendering is global: when any folder needs collapsing, set `navigation.sidebar.display: "group"` once in `blume.config.ts`.
`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`. Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: "group"` (drill-in is `"page"`); when the whole sidebar should collapse, set `navigation.sidebar.display: "group"` once in `blume.config.ts` instead of repeating it per folder.
**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.
@@ -83,7 +83,7 @@ The codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon="
Mintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:
- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — is **not** a `meta.ts` field; it's the global `navigation.sidebar.display` in `blume.config.ts`.)
- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)
- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.
- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.
- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `"GET /path"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).
@@ -44,3 +44,4 @@ Include these sections in the PR body or no-op summary:
- Verification run (commands and results)
- Skipped checks, with reasons
- Remaining risk or follow-up
# chs-e2e-test
+9 -2
View File
@@ -176,7 +176,14 @@ Validate spec structure and auto-fix common issues:
import { validateSpec, autoFixSpec } from "@json-render/core";
const { valid, issues } = validateSpec(spec);
const fixed = autoFixSpec(spec);
// issues include: missing_child, invalid_visible (malformed conditions),
// repeat_without_children, repeat_state_mismatch (statePath not an array in state)
const { spec: fixed, fixDetails } = autoFixSpec(spec);
// fixDetails entries are { message, lossy }. Lossless fixes relocate
// misplaced fields; lossy fixes prune dangling children references.
// In a repair loop, withhold lossy fixes until retries are exhausted:
const attempt = autoFixSpec(spec, { lossy: retriesExhausted });
```
## Visibility Conditions
@@ -253,7 +260,7 @@ The `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`,
| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |
| `EditMode` | Type: `"patch" \| "merge" \| "diff"` |
| `validateSpec` | Validate spec structure |
| `autoFixSpec` | Auto-fix common spec issues |
| `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |
| `createSpecStreamCompiler` | Stream JSONL patches into spec |
| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |
| `parseSpecStreamLine` | Parse single JSONL line |
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: domain-modeling
description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.
description: Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a CONTEXT.md, or recording or editing an ADR.
---
# Domain Modeling
@@ -40,15 +40,20 @@ Description:
Options:
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - Document title (required)
-c, --content <content> - Markdown content (inline)
-f, --content-file <path> - Read content from file
--project <project> - Attach to project (UUID, slug ID, or name)
--issue <issue> - Attach to issue (identifier like TC-123)
--icon <icon> - Document icon (emoji)
-i, --interactive - Interactive mode with prompts
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - Document title (required)
-c, --content <content> - Markdown content (inline)
-f, --content-file <path> - Read content from file
--project <project> - Attach to project (UUID, slug ID, or name)
--issue <issue> - Attach to issue (identifier like TC-123)
--initiative <initiative> - Attach to initiative (UUID, slug ID, or name)
--team <team> - Attach to team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Attach to cycle: name, number, 'active'/'now', 'next', 'previous', or a relative
offset like +1 (team from --team or config)
--release <release> - Attach to release (UUID, name, or version)
--icon <icon> - Document icon (emoji)
-i, --interactive - Interactive mode with prompts
```
### delete
@@ -85,12 +90,17 @@ Description:
Options:
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
--project <project> - Filter by project (slug or name)
--issue <issue> - Filter by issue (identifier like TC-123)
--json - Output as JSON
--limit <limit> - Limit results (Default: 50)
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
--project <project> - Filter by project (UUID, slug ID, or name)
--issue <issue> - Filter by issue (identifier like TC-123)
--initiative <initiative> - Filter by initiative (UUID, slug ID, or name)
--team <team> - Filter by team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Filter by cycle: name, number, 'active'/'now', 'next', 'previous', or a relative
offset like +1 (team from --team or config)
--release <release> - Filter by release (UUID, name, or version)
--json - Output as JSON
--limit <limit> - Limit results (Default: 50)
```
### update
@@ -106,15 +116,21 @@ Description:
Options:
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - New title for the document
-c, --content <content> - New markdown content (inline)
-f, --content-file <path> - Read new content from file
--icon <icon> - New icon (emoji)
--project <project> - Attach to project (UUID, slug ID, or name)
-e, --edit - Open current content in $EDITOR for editing
--force - Update content even when document comments may lose inline anchors
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - New title for the document
-c, --content <content> - New markdown content (inline)
-f, --content-file <path> - Read new content from file
--icon <icon> - New icon (emoji)
--project <project> - Re-point to project (UUID, slug ID, or name); replaces the current attachment
--issue <issue> - Re-point to issue (identifier like TC-123); replaces the current attachment
--initiative <initiative> - Re-point to initiative (UUID, slug ID, or name); replaces the current attachment
--team <team> - Re-point to team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Re-point to cycle: name, number, 'active'/'now', 'next', 'previous', or a
relative offset like +1 (team from --team or config)
--release <release> - Re-point to release (UUID, name, or version); replaces the current attachment
-e, --edit - Open current content in $EDITOR for editing
--force - Update content even when document comments may lose inline anchors
```
### view
+14 -12
View File
@@ -84,19 +84,19 @@ Use these root directories consistently:
- `app/` for runtime application code
- `db/` for migrations and local database files
- `public/` for static assets served as-is
- root `public/` for static assets served as-is from the app root
- `test/` for shared helpers, fixtures, and integration coverage
- `tmp/` for uploads, caches, local session files, and other scratch data
Inside `app/`, organize by responsibility:
- `assets/` for client entrypoints and client-owned browser behavior
- `actions/` for controller-owned route handlers, route-local response rendering, and route-local UI/helpers that are not shared across route areas
- `data/` for schema, queries, persistence setup, migrations, and runtime data initialization
- `middleware/` for request lifecycle concerns such as auth, sessions, uploads, and database injection
- `public/` directories inside the narrowest owner for browser-reachable source code, with the browser runtime entrypoint at `app/actions/public/entry.ts`
- `ui/` for shared cross-route UI primitives
- `utils/` only for genuinely cross-layer helpers that do not clearly belong elsewhere
- `routes.ts` for the route contract
- `routes.ts` for the shared server-and-browser route contract and type-safe href generation
- `router.ts` for router setup and wiring
### Placement Precedence
@@ -148,10 +148,11 @@ When code could live in multiple places:
- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules, and content types are part of the route contract
- Make the server route correct first. A POST should already return the right HTML, redirect, or error response on its own before `clientEntry(...)` layers interactivity on top
- Validate input at the boundary using `remix/data-schema` (and `remix/data-schema/form-data` for forms). `parseSafe` makes the failure path a return value instead of an exception
- Derive `AppContext` from the middleware stack so `get(Database)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness
- Derive `AppContext` from the middleware stack so `get(databaseContext)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness
- Outside actions and controllers, only use `getContext()` when `asyncContext()` is in the middleware stack
- Remix Component is not React: write `function Name(handle: Handle<Props>) { return () => ... }`, read props from `handle.props`, keep state in setup-scope variables, call `handle.update()` explicitly, and do DOM-sensitive work in event handlers or `queueTask(...)`, not in render
- Prefer host-element mixins via `mix={mixin(...)}` for behavior and styling instead of inventing custom host prop conventions. Use `mix={[...]}` only when composing multiple mixins
- Keep short, one-off static styles inline with `mix={css(...)}`. Extract a module-scoped style descriptor when it forms a reused visual recipe, has substantial selectors, media queries, or keyframes, or is large enough to obscure the component. Export a style descriptor only after multiple modules need the same visual recipe; otherwise keep it with its narrowest owner
- Hydrated `clientEntry(...)` props must be serializable. Do not pass functions, class instances, or opaque runtime objects
## Security And Session Defaults
@@ -227,12 +228,12 @@ Use this map to find the right package quickly. Each entry says what the package
- `remix/data-schema/coerce` — coercion helpers for strings, numbers, booleans, dates, and ids. Use when input arrives as a string but should be a typed value
- `remix/data-schema/form-data` — `f.object` and `f.field` for parsing `FormData` directly. Use in actions that read browser forms
- `remix/data-schema/lazy` — recursive or mutually-referential schemas. Use when a schema needs to refer to itself or another schema that is declared later
- `remix/data-table` — typed tables and a `Database` interface. Use for `table`, `column`, `createDatabase` when modeling persisted data
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — adapters. Use to back `createDatabase` with a real engine. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface
- `remix/data-table/migrations` — migration authoring and runners. Use for `createMigration`, `createMigrationRunner`
- `remix/data-table` — typed tables and the shared `Database` API. Use `table` and `column` when modeling persisted data, then create a concrete database from the matching dialect package. Integration packages can implement `DatabaseDriver` and extend `Database` to add another SQL dialect
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — concrete database integrations. Use `createSqliteDatabase`, `createPostgresDatabase`, or `createMysqlDatabase`. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface
- `remix/data-table/migrations` — migration authoring and registries. Use for `createMigration` and `createMigrationRegistry`; run migrations with `Database.migrate()`
- `remix/data-table/migrations/node` — `loadMigrations` from disk. Use in startup scripts that apply migrations
- `remix/data-table/operators` — query operators such as `inList(...)`. Use when `where` clauses need set or comparison logic
- `remix/data-table/sql-helpers` — SQL helper utilities for adapter or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API
- `remix/data-table/sql-helpers` — SQL helper utilities for database integrations or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API
### Auth, Sessions, and Cookies
@@ -318,17 +319,18 @@ export const routes = route({
```typescript
import { createController } from 'remix/router'
import { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts'
export default createController(routes.books, {
actions: {
async index({ get }) {
let db = get(Database)
let db = get(databaseContext)
let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<BooksIndexPage allBooks={allBooks} />)
},
async show({ get, params }) {
let db = get(Database)
let db = get(databaseContext)
let book = await db.findOne(books, { where: { slug: params.slug } })
if (!book) return new Response('Not Found', { status: 404 })
return render(<BookShowPage book={book} />)
@@ -393,8 +395,8 @@ import { redirect } from 'remix/response/redirect'
import * as s from 'remix/data-schema'
import * as f from 'remix/data-schema/form-data'
import { Session } from 'remix/session'
import { Database } from 'remix/data-table'
import { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts'
let bookSchema = f.object({
@@ -410,7 +412,7 @@ export default createController(routes.books, {
return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
}
let db = get(Database)
let db = get(databaseContext)
let book = await db.create(books, parsed.value)
let session = get(Session)
@@ -8,13 +8,13 @@ How to serve browser scripts and styles from source. Read this when the task inv
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need import rewriting, preloads, or fingerprinted URLs
- Generating script URLs or `<link rel="modulepreload">` tags for a client entry
- Enabling browser HMR for source-served modules
- Keeping server-only files out of the browser via `denyFiles` rules
- Keeping files such as tests out of the browser via `denyFiles` rules
For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry hydration and browser update handling, see `hydration-frames-navigation.md`. For the Node HMR runner and browser HMR channel, see `middleware-and-server.md`.
## When To Reach For It
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles under `app/assets/`, and monorepo code that should be compiled and served under a public URL namespace.
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles, and monorepo code that should be compiled and served under a public URL namespace.
Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use `createAssetServer()` for source scripts or styles that need rewriting, dependency scanning, preloads, sourcemaps, or fingerprinted URLs.
@@ -36,9 +36,9 @@ let assetServer = createAssetServer({
'app/*path': 'app/*path',
'node_modules/*path': 'node_modules/*path',
},
allowFiles: ['app/assets/**'],
allowFiles: ['app/routes.ts', 'app/**/public/**'],
allowPackages: ['remix'],
denyFiles: ['app/**/*.server.*'],
denyFiles: ['app/**/*.test.*'],
target: { es: '2020', chrome: '109', safari: '16.4' },
sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
minify: process.env.NODE_ENV === 'production',
@@ -61,10 +61,12 @@ export default createController(routes, {
## Rules
- Treat `allowFiles`/`allowPackages` and `denyFiles` as the security boundary for browser-reachable source files.
- Put browser-reachable app source in a `public/` directory inside `app/`, beside its narrowest owner, such as `app/ui/public/` or `app/actions/cart/public/`.
- Every local dependency in a browser module graph must match `allowFiles`, so keep the whole graph inside those `public/` directories. `app/routes.ts` is allowed separately so browser modules can build type-safe links with `routes.*.href(...)`.
- Deny test modules with `denyFiles` so tests can be colocated inside a `public/` directory without becoming browser-reachable.
- Use `allowFiles` and `denyFiles` for file paths and globs. Relative values resolve from `rootDir`.
- Use `allowPackages` for exact package names, not globs or subpaths. Packages allowed by `allowPackages` also allow their installed `dependencies` and `optionalDependencies`; peer dependencies must be listed explicitly if they should be browser-reachable.
- `denyFiles` takes precedence over both file and package allow rules.
- Add `denyFiles` rules for server-only modules such as `*.server.*`, private config, or other files that should never be exposed.
- Set `rootDir` explicitly in monorepos so relative paths resolve from the intended project root.
- `basePath` is the public URL namespace handled by the asset server.
- `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path patterns. They use `route-pattern` syntax on both sides.
@@ -76,8 +78,8 @@ export default createController(routes, {
Use `getHref()` when you need the public URL for one module, and `getPreloads()` when you want `<link rel="modulepreload">` tags or `Link` headers for one or more entrypoints and their dependencies.
```typescript
let entryHref = await assetServer.getHref('app/assets/entry.ts')
let preloads = await assetServer.getPreloads(['app/assets/entry.ts'])
let entryHref = await assetServer.getHref('app/actions/public/entry.ts')
let entryPreloads = await assetServer.getPreloads('app/actions/public/entry.ts')
```
Use this when rendering documents or layouts that boot browser behavior with a known client entry.
@@ -116,7 +118,8 @@ const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR)
const assetServer = createAssetServer({
basePath: '/assets',
fileMap: { '/app/*path': 'app/*path' },
allowFiles: ['app/assets/**'],
allowFiles: ['app/routes.ts', 'app/**/public/**'],
denyFiles: ['app/**/*.test.*'],
watch: isDevelopment,
hmr: isHmr
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
@@ -137,7 +137,7 @@ Notice that there is no manual `Set-Cookie` plumbing in the action — the sessi
```typescript
import { auth, createSessionAuthScheme } from 'remix/middleware/auth'
import { Session } from 'remix/session'
import { Database } from 'remix/data-table'
import { databaseContext } from '~/middleware/database.ts'
export function loadAuth() {
return auth({
@@ -148,7 +148,7 @@ export function loadAuth() {
return data ?? null
},
async verify(value, context) {
let db = context.get(Database)
let db = context.get(databaseContext)
return (await db.find(users, value.userId)) ?? null
},
invalidate(session) {
@@ -195,7 +195,7 @@ export let passwordProvider = createCredentialsAuthProvider({
return s.parse(loginSchema, formData)
},
async verify({ email, password }, context) {
let db = context.get(Database)
let db = context.get(databaseContext)
let user = await db.findOne(users, { where: { email } })
if (!user || !(await verifyPassword(password, user.password_hash))) {
return null
@@ -300,7 +300,7 @@ export default createController(routes.auth.google, {
async callback(context) {
let { result, returnTo } = await finishExternalAuth(googleProvider, context)
let db = context.get(Database)
let db = context.get(databaseContext)
let { user, authAccount } = await resolveExternalAuth(db, result)
let session = completeAuth(context)
@@ -322,7 +322,7 @@ Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC to
```typescript
async function refreshGoogleTokens({ get }) {
let db = get(Database)
let db = get(databaseContext)
let account = await db.findOne(authAccounts, { where: { provider: 'google' } })
if (!account) return null
@@ -113,30 +113,30 @@ export const books = table({
## Database Setup
Create a database with an adapter and expose it via middleware:
Create a database and expose it via middleware:
```typescript
import BetterSqlite3 from 'better-sqlite3'
import { createDatabase, Database } from 'remix/data-table'
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
import { createSqliteDatabase } from 'remix/data-table/sqlite'
let sqlite = new BetterSqlite3('./db/app.db')
sqlite.pragma('foreign_keys = ON')
let adapter = createSqliteDatabaseAdapter(sqlite)
export let db = createDatabase(adapter)
export let db = createSqliteDatabase(sqlite)
```
`createSqliteDatabaseAdapter` accepts synchronous SQLite clients with a shared `prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever client fits the runtime instead of assuming `better-sqlite3` is required.
`createSqliteDatabase` accepts synchronous SQLite clients with a shared `prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever client fits the runtime instead of assuming `better-sqlite3` is required.
### Database middleware
```typescript
import type { Middleware } from 'remix/router'
import { Database } from 'remix/data-table'
import type { Database } from 'remix/data-table'
import { createContextKey, type Middleware } from 'remix/router'
export const databaseContext = createContextKey<Database>()
export function loadDatabase(): Middleware {
return async (context, next) => {
context.set(Database, db)
context.set(databaseContext, db)
return next()
}
}
@@ -145,7 +145,7 @@ export function loadDatabase(): Middleware {
### Querying
```typescript
let db = get(Database)
let db = get(databaseContext)
// Find by primary key
let book = await db.find(books, id)
@@ -220,7 +220,7 @@ Do **not** import app code (e.g. `app/data/schema.ts`) into migration files. Mig
### Transaction modes
Migrations run inside a transaction by default (when the adapter supports transactional DDL). Override per migration with a directive comment in `up.sql`:
Migrations run inside a transaction by default (when the database supports transactional DDL). Override per migration with a directive comment in `up.sql`:
```sql
-- data-table/transaction: none
@@ -232,15 +232,15 @@ Modes: `auto` (default — wrap when supported), `required` (wrap; throw if unsu
### Running migrations
```typescript
import { createMigrationRunner } from 'remix/data-table/migrations'
import { loadMigrations } from 'remix/data-table/migrations/node'
let migrations = await loadMigrations('./db/migrations')
let runner = createMigrationRunner(adapter, migrations)
await runner.up()
await db.migrate(migrations)
```
The runner checksums each `up.sql` and detects drift if a previously applied migration changes. Use `runner.status()` to inspect applied/pending/drifted state, and `runner.down()` to revert.
The database checksums each `up.sql` and detects drift if a previously applied migration changes.
Use `db.migrationStatus(migrations)` to inspect applied/pending/drifted state, and
`db.migrate(migrations, { direction: 'down' })` to revert.
## Input Validation (`remix/data-schema`)
@@ -95,6 +95,7 @@ Client entry props must be serializable: strings, numbers, booleans, `null`, `un
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
```tsx
import type { ResolveFrameOptions } from 'remix/ui'
import { run } from 'remix/ui'
const app = run({
@@ -102,14 +103,31 @@ const app = run({
let mod = await import(moduleUrl)
return mod[exportName]
},
async resolveFrame(src, signal, target) {
let headers = new Headers({ accept: 'text/html' })
if (target) headers.set('x-remix-target', target)
let response = await fetch(src, { headers, signal })
async resolveFrame(src, options) {
let headers = new Headers({ accept: 'text/html', 'x-remix-frame': 'true' })
if (options?.target) headers.set('x-remix-target', options.target)
let response = await fetch(src, {
body: getRequestBody(options),
headers,
method: options?.method,
signal: options?.signal,
})
return response.body ?? (await response.text())
},
})
function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
let formData = options?.formData
if (!formData) return
if (options.encType !== 'application/x-www-form-urlencoded') return formData
let body = new URLSearchParams()
for (let [name, value] of formData) {
body.append(name, typeof value === 'string' ? value : value.name)
}
return body
}
app.addEventListener('error', (event) => {
console.error('Component error:', event.error)
})
@@ -120,7 +138,7 @@ await app.ready()
### `run` options
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
- **`resolveFrame(src, signal, target)`** (optional) — called when a `<Frame>` loads or reloads content. `target` is available when frame targeting matters.
- **`resolveFrame(src, options)`** (optional) — called when a `<Frame>` loads or reloads content and for intercepted link and form navigations. `options` may contain `signal` and `target`; non-GET forms also provide `formData`, `method`, and `encType`.
### `app` methods
@@ -189,6 +207,20 @@ handle.frames.top.reload()
When a frame reloads, matching DOM nodes are updated in place. Client entries receive updated props while preserving their local component state.
### Form navigation
When `run({ resolveFrame })` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
- Forms target `handle.frames.top` by default.
- `rmx-target` selects a named frame.
- `rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
- `rmx-history="push|replace"` overrides how the navigation updates history.
- `rmx-reset-scroll="false"` preserves scroll position.
- `rmx-document` opts back into a document submission.
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
GET controls are already encoded in `src`, so GET forms reach the resolver like links. Non-GET forms provide their native `FormData`, effective method, and encoding. The resolver owns body encoding and method-override conventions. Non-GET submissions to the current URL replace its history entry; GET submissions and submissions to a different URL push one. The `rmx-history` attribute overrides those defaults.
### Nested frames
Frames can nest. Each frame owns its own DOM region and hydrates client entries independently. During SSR, `handle.frame.src` points at the frame being rendered, while `handle.frames.top.src` stays fixed at the outer document URL.
@@ -252,7 +284,7 @@ navigate('/dashboard', { history: 'replace' })
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-document`.
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-history`, `rmx-reset-scroll`, `rmx-document`.
## Head Management
@@ -65,8 +65,9 @@ let router = createRouter({ middleware })
### Static files vs browser modules
- Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in `public/`
- Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in the root `public/` directory
- Use `remix/assets` when browser modules should be compiled and served from source files with import rewriting, preloads, or fingerprinted URLs
- `public/` directories inside `app/` hold browser-reachable source for the asset server
### Ordering notes
@@ -118,11 +119,11 @@ Use `context.set(key, value)` to add typed values accessible downstream via `con
```typescript
import type { Middleware } from 'remix/router'
import { Database } from 'remix/data-table'
import { databaseContext } from '~/middleware/database.ts'
export function loadDatabase(): Middleware {
return async (context, next) => {
context.set(Database, db)
context.set(databaseContext, db)
return next()
}
}
@@ -152,11 +153,11 @@ export function requireAdmin(): Middleware {
// app/utils/context.ts
import { getContext } from 'remix/middleware/async-context'
import { Auth } from 'remix/middleware/auth'
import { Database } from 'remix/data-table'
import { databaseContext } from '~/middleware/database.ts'
import { Session } from 'remix/session'
export function getCurrentDb() {
return getContext().get(Database)
return getContext().get(databaseContext)
}
export function getCurrentSession() {
@@ -115,6 +115,8 @@ Adds client-side navigation behavior to any element. Makes non-anchor elements b
Options match `NavigationOptions`: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
On a native anchor, the `history` option renders the corresponding `rmx-history="push|replace"` attribute so the enhanced navigation uses the same history behavior.
## Native press and keyboard interactions
Use native DOM events directly with `on(...)`. For buttons and links, `click` already includes keyboard activation when the element has the right semantics:
@@ -103,7 +103,7 @@ export const search = createAction(routes.search, {
The handler receives a context object with:
- `get(key)` — read a value set by middleware (e.g. `get(Database)`, `get(Session)`, `get(Auth)`)
- `get(key)` — read a value set by middleware (e.g. `get(databaseContext)`, `get(Session)`, `get(Auth)`)
- `params` — typed route params
- `url` — the request URL
- `request` — the raw `Request`
@@ -132,7 +132,7 @@ For pages, render a component tree and return the resulting `Response`:
```typescript
async handler({ get }) {
let db = get(Database)
let db = get(databaseContext)
let books = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<IndexPage books={books} />)
}
@@ -152,7 +152,7 @@ async create({ get }) {
return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
}
let db = get(Database)
let db = get(databaseContext)
let book = await db.create(books, parsed.value)
return redirect(routes.books.show.href({ slug: book.slug }), 303)
@@ -167,7 +167,7 @@ For expected failures — validation, conflict, not found — return a `Response
```typescript
async show({ get, params }) {
let db = get(Database)
let db = get(databaseContext)
let book = await db.find(books, params.bookId)
if (!book) return new Response('Not Found', { status: 404 })
return render(<ShowPage book={book} />)
@@ -208,7 +208,7 @@ If you find yourself returning JSON for what is really a browser form submission
A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers.
Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(databaseContext)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
```typescript
import { createController } from 'remix/router'
@@ -218,13 +218,13 @@ import { routes } from '../routes.ts'
export default createController(routes.books, {
actions: {
async index({ get }) {
let db = get(Database)
let db = get(databaseContext)
let items = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<IndexPage items={items} />)
},
async show({ get, params }) {
let db = get(Database)
let db = get(databaseContext)
let book = await db.find(books, params.bookId)
if (!book) return new Response('Not Found', { status: 404 })
return render(<ShowPage book={book} />)
@@ -359,4 +359,4 @@ declare module 'remix/router' {
}
```
This gives typed `context.get(Database)`, `context.get(Session)`, `context.get(Auth)`, etc.
This gives typed `context.get(databaseContext)`, `context.get(Session)`, `context.get(Auth)`, etc.
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: rust-testing
description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology.
description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology. Use when writing Rust tests — unit, integration, async, property-based, or coverage.
metadata:
origin: ECC
---