mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-05 20:17:42 +00:00
chs: re-add drifted files
dot_agents/dot_skill-lock.json
This commit is contained in:
1 parent
03ef5c94ea
commit
42d17e825e
30 files changed
+230
-145
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user