-
} />
+
+
+ )
+}
+
+let stream = renderToStream(
, {
+ resolveFrame: (src) => fetchFrameHtml(src),
+})
+
+return new Response(stream, {
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+})
+```
+
+### Client entry module
+
+```tsx
+// assets/entry.tsx
+import { run } from 'remix/component'
+
+let app = run({
+ async loadModule(moduleUrl, exportName) {
+ let mod = await import(moduleUrl)
+ return mod[exportName]
+ },
+ async resolveFrame(src, signal) {
+ let res = await fetch(src, { headers: { accept: 'text/html' }, signal })
+ return res.body ?? (await res.text())
+ },
+})
+
+await app.ready()
+```
+
+### Client entry component
+
+```tsx
+// assets/counter.tsx
+import { clientEntry, on, type Handle } from 'remix/component'
+
+export let Counter = clientEntry(
+ '/assets/counter.js#Counter',
+ function Counter(handle: Handle, setup: number) {
+ let count = setup
+
+ return (props: { label: string }) => (
+
+
+ {props.label}: {count}
+
+
+
+ )
+ },
+)
+```
+
+## Next Steps
+
+- [Components](./components.md) - Component structure and runtime behavior
+- [Handle API](./handle.md) - The component's interface to the framework
+- [Server Rendering](./server-rendering.md) - `renderToString` and
+ `renderToStream`
+- [Hydration](./hydration.md) - `clientEntry` and `run`
+- [Frames](./frames.md) - Streaming partial server UI with `
`
+- [Styling](./styling.md) - CSS mixin for inline styling
+- [Events](./events.md) - Event handling patterns
diff --git a/docs/agents/remix/component/docs/handle.md b/docs/agents/remix/component/docs/handle.md
new file mode 100644
index 0000000..6fec17d
--- /dev/null
+++ b/docs/agents/remix/component/docs/handle.md
@@ -0,0 +1,343 @@
+# Handle API
+
+The `Handle` object provides the component's interface to the framework.
+
+## `handle.update()`
+
+Schedules a component update and returns a promise that resolves with an
+`AbortSignal` after the update completes.
+
+```tsx
+function Counter(handle: Handle) {
+ let count = 0
+
+ return () => (
+
+ )
+}
+```
+
+Waiting for the update:
+
+```tsx
+function Player(handle: Handle) {
+ let isPlaying = false
+ let stopButton: HTMLButtonElement
+
+ return () => (
+
+ )
+}
+```
+
+## `handle.queueTask(task)`
+
+Schedules a task to run after the next update. The task receives an
+`AbortSignal` that's aborted when:
+
+- The component re-renders (new render cycle starts)
+- The component is removed from the tree
+
+**Use `queueTask` in event handlers when work needs to happen after DOM
+changes:**
+
+```tsx
+function Form(handle: Handle) {
+ let showDetails = false
+ let detailsSection: HTMLElement
+
+ return () => (
+
+ )
+}
+```
+
+**Use `queueTask` for work that needs to be reactive to prop changes:**
+
+When you need to perform async work (like data fetching) that should respond to
+prop changes, use `queueTask` in the render function. The signal will be aborted
+if props change or the component is removed, ensuring only the latest work
+completes.
+
+### Anti-patterns
+
+**Don't create states as values to "react to" on the next render with
+`queueTask`:**
+
+```tsx
+// ❌ Avoid: Creating state just to react to it in queueTask
+function BadExample(handle: Handle) {
+ let shouldLoad = false // Unnecessary state
+
+ return () => (
+
+
+
+ )
+}
+
+// ✅ Prefer: Do the work directly in the event handler or queueTask
+function GoodExample(handle: Handle) {
+ return () => (
+
+
+
+ )
+}
+```
+
+**When showing loading state before async work, await `handle.update()` and use
+the returned signal:**
+
+```tsx
+function AsyncExample(handle: Handle) {
+ let data: string[] = []
+ let loading = false
+
+ async function load() {
+ loading = true
+ let signal = await handle.update()
+
+ let response = await fetch('/api/data', { signal })
+ if (signal.aborted) return
+
+ data = await response.json()
+ loading = false
+ handle.update()
+ }
+
+ return () => (
+
+ )
+}
+```
+
+## `handle.signal`
+
+An `AbortSignal` that's aborted when the component is disconnected. Useful for
+cleanup operations.
+
+```tsx
+function Clock(handle: Handle) {
+ let interval = setInterval(() => {
+ if (handle.signal.aborted) {
+ clearInterval(interval)
+ return
+ }
+ handle.update()
+ }, 1000)
+
+ return () =>
{new Date().toString()}
+}
+```
+
+Or using event listeners:
+
+```tsx
+function Clock(handle: Handle) {
+ let interval = setInterval(handle.update, 1000)
+ handle.signal.addEventListener('abort', () => clearInterval(interval))
+
+ return () =>
{new Date().toString()}
+}
+```
+
+## `addEventListeners(target, handle.signal, listeners)`
+
+Listen to an `EventTarget` with automatic cleanup when the component
+disconnects. Ideal for global event targets like `document` and `window`.
+
+```tsx
+function KeyboardTracker(handle: Handle) {
+ let keys: string[] = []
+
+ addEventListeners(document, handle.signal, {
+ keydown(event) {
+ keys.push(event.key)
+ handle.update()
+ },
+ })
+
+ return () =>
Keys: {keys.join(', ')}
+}
+```
+
+## `handle.frames.top`
+
+The root frame for the current runtime tree. This is useful when nested
+components need to reload the entire page/frame tree instead of only their
+nearest frame.
+
+When server rendering with `renderToStream()`, pass the `frameSrc` option to
+populate `handle.frames.top.src` during SSR. For nested frame renders, also pass
+`topFrameSrc` to keep the top-frame URL fixed while `handle.frame.src` changes
+per frame.
+
+```tsx
+function RefreshAllButton(handle: Handle) {
+ return () => (
+
+ )
+}
+```
+
+## `handle.frames.get(name)`
+
+Look up a named frame in the current runtime tree. This is useful when one frame
+action should refresh adjacent frame content.
+
+Return value:
+
+- `FrameHandle` when a frame with that `name` is currently mounted
+- `undefined` when no such frame is mounted
+
+```tsx
+function CartRow(handle: Handle) {
+ return () => (
+
+ )
+}
+```
+
+If multiple mounted frames share the same name, the most recently mounted frame
+is returned.
+
+## `handle.id`
+
+Stable identifier per component instance. Useful for HTML APIs like `htmlFor`,
+`aria-owns`, etc.
+
+```tsx
+function LabeledInput(handle: Handle) {
+ return () => (
+
+
+
+
+ )
+}
+```
+
+## `handle.context`
+
+Context API for ancestor/descendant communication. See [Context](./context.md)
+for full documentation.
+
+```tsx
+function App(handle: Handle<{ theme: string }>) {
+ handle.context.set({ theme: 'dark' })
+
+ return () => (
+
+
+
+
+ )
+}
+
+function Header(handle: Handle) {
+ let { theme } = handle.context.get(App)
+ return () => (
+
+ )
+}
+```
+
+**Important:** `handle.context.set()` does not cause any updates—it simply
+stores a value. If you need the component tree to update when context changes,
+call `handle.update()` after setting the context.
+
+## See Also
+
+- [Events](./events.md) - Event handling patterns with signals
+- [Context](./context.md) - Context API with TypedEventTarget
+- [Patterns](./patterns.md) - Common usage patterns
diff --git a/docs/agents/remix/component/docs/hydration.md b/docs/agents/remix/component/docs/hydration.md
new file mode 100644
index 0000000..16e5690
--- /dev/null
+++ b/docs/agents/remix/component/docs/hydration.md
@@ -0,0 +1,135 @@
+# Hydration
+
+Hydration makes server-rendered HTML interactive on the client. You mark
+specific components as **client entries**, and the client runtime finds them in
+the page, loads their code, and hydrates them in place.
+
+Only the components you mark are hydrated. The rest of the page stays as static
+HTML.
+
+## Defining a client entry
+
+Use `clientEntry` to mark a component for hydration. The first argument is the
+module URL and export name the client will use to load the component:
+
+```tsx
+import { clientEntry, on, type Handle } from 'remix/component'
+
+export let Counter = clientEntry(
+ '/assets/counter.js#Counter',
+ function Counter(handle: Handle, setup: number) {
+ let count = setup
+
+ return (props: { label: string }) => (
+
+
+ {props.label}: {count}
+
+
+
+ )
+ },
+)
+```
+
+The format is `moduleUrl#ExportName`. If you omit the export name, the
+function's name is used as a fallback.
+
+On the server, `clientEntry` components render like any other component. The
+server wraps their output in comment markers and serializes their props into a
+`'
+let response = createHtmlResponse(html`
${unsafe}
`, { status: 400 })
+```
+
+The `html.raw` template tag can be used to interpolate values without escaping
+them. This has the same semantics as `String.raw` but for HTML snippets that
+have already been escaped or are from trusted sources:
+
+```ts
+// Use html.raw as a template tag to skip escaping interpolations
+let safeHtml = '
Bold'
+let content = html.raw`
${safeHtml}
`
+let response = createHtmlResponse(content)
+
+// This is particularly useful when building HTML from multiple safe fragments
+let header = '
'
+let body = '
Content'
+let footer = '
'
+let page = html.raw`
+
+
+
+ ${header}
+ ${body}
+ ${footer}
+
+
+`
+
+// You can nest html.raw inside html to preserve SafeHtml fragments
+let icon = html.raw`
`
+let button = html`
` // icon is not escaped
+```
+
+**Warning**: Only use `html.raw` with trusted content. Unlike the regular `html`
+template tag, `html.raw` does not escape its interpolations, which can lead to
+XSS vulnerabilities if used with untrusted user input.
+
+See the
+[`html-template` documentation](https://github.com/remix-run/remix/tree/main/packages/html-template#readme)
+for more details.
+
+### Testing
+
+Testing is straightforward because `fetch-router` uses the standard `fetch()`
+API:
+
+```ts
+import * as assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+
+describe('blog routes', () => {
+ it('creates a new post', async () => {
+ let response = await router.fetch('https://api.remix.run/posts', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ title: 'Hello', content: 'World' }),
+ })
+
+ assert.equal(response.status, 201)
+ let post = await response.json()
+ assert.equal(post.title, 'Hello')
+ })
+
+ it('returns 404 for missing posts', async () => {
+ let response = await router.fetch('https://api.remix.run/posts/not-found')
+ assert.equal(response.status, 404)
+ })
+})
+```
+
+No special test harness or mocking required! Just use `fetch()` like you would
+in production.
+
+## Related Packages
+
+- [auth-middleware](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) -
+ Request authentication and route protection helpers
+- [session-middleware](https://github.com/remix-run/remix/tree/main/packages/session-middleware) -
+ Load and persist sessions in request context
+- [form-data-middleware](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) -
+ Parse request bodies into `context.get(FormData)`
+- [response](https://github.com/remix-run/remix/tree/main/packages/response) -
+ Response helpers for HTML, JSON, files, and redirects
+
+## Related Work
+
+- [headers](https://github.com/remix-run/remix/tree/main/packages/headers) - A
+ library for working with HTTP headers
+- [form-data-parser](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
+ A library for parsing multipart/form-data requests
+- [route-pattern](https://github.com/remix-run/remix/tree/main/packages/route-pattern) -
+ The pattern matching library that powers `fetch-router`
+- [Express](https://expressjs.com/) - The classic Node.js web framework
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/fetch-router/advanced-topics.md b/docs/agents/remix/fetch-router/advanced-topics.md
deleted file mode 100644
index 6237c9c..0000000
--- a/docs/agents/remix/fetch-router/advanced-topics.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Additional topics and HTML helpers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Additional topics
-
-### Scaling your application
-
-- how to use a TrieMatcher
-- how to spread controllers across multiple files
-
-### Error handling and aborted requests
-
-- wrap `router.fetch()` in a try/catch to handle errors
-- `AbortError` is thrown when a request is aborted
-
-### Content negotiation
-
-- use `Accept.from()` from `@remix-run/headers` to serve different responses
- based on the client's `Accept` header
- - maybe put this on `context.accepts()` for convenience?
-
-### Sessions
-
-- use a custom `sessionStorage` implementation to store session data
-- use `session.get()` and `session.set()` to get and set session data
-- use `session.flash()` to set a flash message
-- use `session.destroy()` to destroy the session
-
-### Form data and file uploads
-
-- use the `formData()` middleware to parse the `FormData` object from the
- request body
-- use the `formData` property of the context object to access the form data
-- use the `files` property of the context object to access the uploaded files
-- use the `uploadHandler` option of the `formData()` middleware to handle file
- uploads
-
-### Request method override
-
-- use the `methodOverride()` middleware to override the request method
-- use a hidden `
` to override the request
- method
-
-## Response helpers
-
-Response helpers for creating common HTTP responses are available in the
-[`@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response)
-package:
-
-```tsx
-import { createFileResponse } from '@remix-run/response/file'
-import { createHtmlResponse } from '@remix-run/response/html'
-import { createRedirectResponse } from '@remix-run/response/redirect'
-import { compressResponse } from '@remix-run/response/compress'
-
-let response = createHtmlResponse('
Hello
')
-let response = Response.json({ message: 'Hello' })
-let response = createRedirectResponse('/')
-let response = compressResponse(uncompressedResponse, request)
-```
-
-See the
-[`@remix-run/response` documentation](https://github.com/remix-run/remix/tree/main/packages/response#readme)
-for more details.
-
-## Working with HTML
-
-For working with HTML strings and safe HTML interpolation, see the
-[`@remix-run/html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template)
-package. It provides a `html` template tag with automatic escaping to prevent
-XSS vulnerabilities.
-
-```ts
-import { html } from '@remix-run/html-template'
-import { createHtmlResponse } from '@remix-run/response/html'
-
-// Use the template tag to escape unsafe variables in HTML.
-let unsafe = ''
-let response = createHtmlResponse(html`
${unsafe}
`, { status: 400 })
-```
-
-The `html.raw` template tag can be used to interpolate values without escaping
-them. This has the same semantics as `String.raw` but for HTML snippets that
-have already been escaped or are from trusted sources:
-
-```ts
-// Use html.raw as a template tag to skip escaping interpolations
-let safeHtml = '
Bold'
-let content = html.raw`
${safeHtml}
`
-let response = createHtmlResponse(content)
-
-// This is particularly useful when building HTML from multiple safe fragments
-let header = '
'
-let body = '
Content'
-let footer = '
'
-let page = html.raw`
-
-
-
- ${header}
- ${body}
- ${footer}
-
-
-`
-
-// You can nest html.raw inside html to preserve SafeHtml fragments
-let icon = html.raw`
`
-let button = html`
` // icon is not escaped
-```
-
-**Warning**: Only use `html.raw` with trusted content. Unlike the regular `html`
-template tag, `html.raw` does not escape its interpolations, which can lead to
-XSS vulnerabilities if used with untrusted user input.
-
-See the
-[`@remix-run/html-template` documentation](https://github.com/remix-run/remix/tree/main/packages/html-template#readme)
-for more details.
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Middleware and request context](./middleware.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/index.md b/docs/agents/remix/fetch-router/index.md
deleted file mode 100644
index 0b6f0e7..0000000
--- a/docs/agents/remix/fetch-router/index.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# fetch-router
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Overview
-
-A minimal, composable router built on the
-[web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and
-[`route-pattern`](../route-pattern). Ideal for building APIs, web services, and
-server-rendered applications across any JavaScript runtime.
-
-## Features
-
-- **Fetch API**: Built on standard web APIs that work everywhere - Node.js, Bun,
- Deno, Cloudflare Workers, and browsers
-- **Type-Safe Routing**: Leverage TypeScript for compile-time route validation
- and parameter inference
-- **Composable Architecture**: Nest routers, combine middleware, and organize
- routes hierarchically
-- **Declarative Route Maps**: Define your entire route structure upfront with
- type-safe route names and request methods
-- **Flexible Middleware**: Apply middleware globally, per-route, or to entire
- route hierarchies
-- **Easy Testing**: Use standard `fetch()` to test your routes - no special test
- harness required
-
-## Goals
-
-- **Simplicity**: A router should be simple to understand and use. The entire
- API surface fits in your head.
-- **Composability**: Small routers combine to build large applications.
- Middleware and nested routers make organization natural.
-- **Standards-Based**: Built on web standards that work across runtimes. No
- proprietary APIs or Node.js-specific code.
-
-## Installation
-
-```sh
-npm i remix
-```
-
-Import route definition helpers from `remix/fetch-router/routes`, and runtime
-APIs from `remix/fetch-router`.
-
-## Navigation
-
-- [Basic usage and route maps](./usage.md)
-- [Routing based on request method](./routing-methods.md)
-- [Resource-based routes](./routing-resources.md)
-- [Middleware and request context](./middleware.md)
-- [Additional topics and HTML helpers](./advanced-topics.md)
-- [Testing and related work](./testing-and-related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/middleware.md b/docs/agents/remix/fetch-router/middleware.md
deleted file mode 100644
index 14b0110..0000000
--- a/docs/agents/remix/fetch-router/middleware.md
+++ /dev/null
@@ -1,130 +0,0 @@
-# Middleware and request context
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Controllers and middleware
-
-Middleware functions run code before and/or after actions. They are a powerful
-way to add functionality to your app.
-
-A basic logging middleware might look like this:
-
-```ts
-import type { Middleware } from 'remix/fetch-router'
-
-// You can use the `Middleware` type to type middleware functions.
-function logger(): Middleware {
- return async (context, next) => {
- let start = new Date()
-
- // Call next() to invoke the next middleware or action in the chain.
- let response = await next()
-
- let end = new Date()
- let duration = end.getTime() - start.getTime()
-
- console.log(
- `${context.request.method} ${context.request.url} ${response.status} ${duration}ms`,
- )
-
- return response
- }
-}
-
-// Use it like this:
-let router = createRouter({
- middleware: [logger()],
-})
-```
-
-Middleware is typically built as a function that returns a middleware function.
-This allows you to pass options to the middleware function if needed. For
-example, the `auth()` middleware below allows you to pass a `token` option that
-is used to authenticate the request.
-
-```tsx
-interface AuthOptions {
- token: string
-}
-
-function auth(options?: AuthOptions): Middleware {
- let token = options?.token ?? 'secret'
-
- return (context, next) => {
- if (context.headers.get('Authorization') !== `Bearer ${token}`) {
- return new Response('Unauthorized', { status: 401 })
- }
- return next()
- }
-}
-```
-
-Middleware may be used in two different contexts: globally (at the router level)
-or inline (at the route level).
-
-Global middleware is added to the router when it is created using the
-`createRouter({ middleware })` option. This middleware runs before any routes
-are matched and is useful for doing things like logging, serving static files,
-profiling, and a variety of other things. Global middleware runs on every
-request, so it's important to keep them lightweight and fast.
-
-Inline (or "route") middleware is added to the router when actions are
-registered using either `router.map()` or one of the method-specific helpers
-like `router.get()`, `router.post()`, `router.put()`, `router.delete()`, etc.
-Route middleware runs after global middleware but before the route action, and
-is useful for doing things like authentication, authorization, and data
-validation.
-
-```tsx
-let routes = route({
- home: '/',
- admin: {
- dashboard: '/admin/dashboard',
- },
-})
-
-let router = createRouter({
- // This middleware runs on all requests.
- middleware: [staticFiles('./public')],
-})
-
-router.map(routes.home, () => new Response('Home'))
-
-router.map(routes.admin.dashboard, {
- // This middleware runs only on the `/admin/dashboard` route.
- middleware: [auth({ token: 'secret' })],
- action() {
- return new Response('Dashboard')
- },
-})
-```
-
-## Request context
-
-Every action and middleware receives a `context` object with useful properties:
-
-```ts
-router.get('/posts/:id', ({ request, url, params, storage }) => {
- // request: The original Request object
- console.log(request.method) // "GET"
- console.log(request.headers.get('Accept'))
-
- // url: Parsed URL object
- console.log(url.pathname) // "/posts/123"
- console.log(url.searchParams.get('sort'))
-
- // params: Route parameters (fully typed!)
- console.log(params.id) // "123"
-
- // storage: AppStorage for type-safe access to request-scoped data
- storage.set('user', currentUser)
-
- return new Response(`Post ${params.id}`)
-})
-```
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Routing based on request method](./routing-methods.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/routing-methods.md b/docs/agents/remix/fetch-router/routing-methods.md
deleted file mode 100644
index 62e183d..0000000
--- a/docs/agents/remix/fetch-router/routing-methods.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# Routing based on request method
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Routing based on request method
-
-In the example above, both the `home` and `contact` routes are able to be
-registered for any incoming
-[`request.method`](https://developer.mozilla.org/en-US/docs/Web/API/Request/method).
-If you inspect their types, you'll see:
-
-```tsx
-type HomeRoute = typeof routes.home // Route<'ANY', '/'>
-type ContactRoute = typeof routes.contact // Route<'ANY', '/contact'>
-```
-
-We used `router.get()` and `router.post()` to register actions on each route
-specifically for the `GET` and `POST` request methods.
-
-However, we can also encode the request method into the route definition itself
-using the `method` property on the route. When you include the `method` in the
-route definition, `router.map()` will register the action only for that specific
-request method. This can be more convenient than using `router.get()` and
-`router.post()` to register actions one at a time.
-
-```ts
-import * as assert from 'node:assert/strict'
-import { createRouter } from 'remix/fetch-router'
-import { route } from 'remix/fetch-router/routes'
-
-let routes = route({
- home: { method: 'GET', pattern: '/' },
- contact: {
- index: { method: 'GET', pattern: '/contact' },
- action: { method: 'POST', pattern: '/contact' },
- },
-})
-
-type Routes = typeof routes
-// Each route is now typed with a specific request method.
-// {
-// home: Route<'GET', '/'>,
-// contact: {
-// index: Route<'GET', '/contact'>,
-// action: Route<'POST', '/contact'>,
-// },
-// }
-
-let router = createRouter()
-
-router.map(routes, {
- home({ method }) {
- assert.equal(method, 'GET')
- return new Response('Home')
- },
- contact: {
- index({ method }) {
- assert.equal(method, 'GET')
- return new Response('Contact')
- },
- action({ method }) {
- assert.equal(method, 'POST')
- return new Response('Contact Action')
- },
- },
-})
-```
-
-## Declaring routes
-
-In additon to the `{ method, pattern }` syntax shown above, the router provides
-a few shorthand methods that help to eliminate some of the boilerplate when
-building complex route maps:
-
-- [`form`](#declaring-form-routes) - creates a route map with an `index` (`GET`)
- and `action` (`POST`) route. This is well-suited to showing a standard HTML
- `
-
-
- `)
- },
- // POST /contact - handles the form submission
- action({ formData }) {
- let message = formData.get('message') as string
- let body = html`
-
-
-
Thanks!
-
You said: ${message}
-
-
- Got more to say?
- Send another message
-
-
-
- `
-
- return createHtmlResponse(body)
- },
- },
-})
-```
-
-## Navigation
-
-- [Resource-based routes](./routing-resources.md)
-- [Basic usage and route maps](./usage.md)
-- [fetch-router overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/routing-resources.md b/docs/agents/remix/fetch-router/routing-resources.md
deleted file mode 100644
index 150b7e5..0000000
--- a/docs/agents/remix/fetch-router/routing-resources.md
+++ /dev/null
@@ -1,186 +0,0 @@
-# Resource-based routes
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Resource-based routes
-
-The router provides a `resources()` helper that creates a route map with a set
-of resource-based routes, useful when defining RESTful API routes or modeling
-resources in a web application (similar to Rails' `resources` helper).
-
-```ts
-import { createRouter } from 'remix/fetch-router'
-import { resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- brands: {
- ...resources('brands', { only: ['index', 'show'] }),
- products: resources('brands/:brandId/products', {
- only: ['index', 'show'],
- }),
- },
-})
-
-type Routes = typeof routes
-// {
-// brands: {
-// index: Route<'GET', '/brands'>
-// show: Route<'GET', '/brands/:id'>
-// products: {
-// index: Route<'GET', '/brands/:brandId/products'>
-// show: Route<'GET', '/brands/:brandId/products/:id'>
-// },
-// },
-// }
-
-let router = createRouter()
-
-router.map(routes.brands, {
- // GET /brands
- index() {
- return new Response('Brands Index')
- },
- // GET /brands/:id
- show({ params }) {
- return new Response(`Brand ${params.id}`)
- },
- products: {
- // GET /brands/:brandId/products
- index() {
- return new Response('Products Index')
- },
- // GET /brands/:brandId/products/:id
- show({ params }) {
- return new Response(`Brand ${params.brandId}, Product ${params.id}`)
- },
- },
-})
-```
-
-The `resource()` helper creates a route map for a single resource (not something
-that is part of a collection). This is useful when defining operations on a
-singleton resource, like a user profile.
-
-```tsx
-import { createRouter } from 'remix/fetch-router'
-import { resource, resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- user: {
- ...resources('users', { only: ['index', 'show'] }),
- profile: resource('users/:userId/profile', {
- only: ['show', 'edit', 'update'],
- }),
- },
-})
-
-type Routes = typeof routes
-// {
-// user: {
-// index: Route<'GET', '/users'>
-// show: Route<'GET', '/users/:id'>
-// profile: {
-// show: Route<'GET', '/users/:userId/profile'>
-// edit: Route<'GET', '/users/:userId/profile/edit'>
-// update: Route<'PUT', '/users/:userId/profile'>
-// },
-// },
-// }
-```
-
-Without the `only` option, a `resources('users')` route map contains 7 routes:
-`index`, `new`, `show`, `create`, `edit`, `update`, and `destroy`.
-
-```tsx
-let routes = resources('users')
-type Routes = typeof routes
-// {
-// index: Route<'GET', '/users'> - Lists all users
-// new: Route<'GET', '/users/new'> - Shows a form to create a new user
-// show: Route<'GET', '/users/:id'> - Shows a single user
-// create: Route<'POST', '/users'> - Creates a new user
-// edit: Route<'GET', '/users/:id/edit'> - Shows a form to edit a user
-// update: Route<'PUT', '/users/:id'> - Updates a user
-// destroy: Route<'DELETE', '/users/:id'> - Deletes a user
-// }
-```
-
-Similarly, a `resource('profile')` route map contains 6 routes: `new`, `show`,
-`create`, `edit`, `update`, and `destroy`. There is no `index` route because a
-`resource()` represents a singleton resource, not a collection, so there is no
-collection view.
-
-```tsx
-let routes = resource('profile')
-type Routes = typeof routes
-// {
-// new: Route<'GET', '/profile/new'> - Shows a form to create the profile
-// show: Route<'GET', '/profile'> - Shows the profile
-// create: Route<'POST', '/profile'> - Creates the profile
-// edit: Route<'GET', '/profile/edit'> - Shows a form to edit the profile
-// update: Route<'PUT', '/profile'> - Updates the profile
-// destroy: Route<'DELETE', '/profile'> - Deletes the profile
-// }
-```
-
-Resource route names may be customized using the `names` option when you'd
-prefer not to use the default
-`index`/`new`/`show`/`create`/`edit`/`update`/`destroy` route names.
-
-```tsx
-import { createRouter } from 'remix/fetch-router'
-import { resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- users: resources('users', {
- only: ['index', 'show'],
- names: { index: 'list', show: 'view' },
- }),
-})
-type Routes = typeof routes.users
-// {
-// list: Route<'GET', '/users'> - Lists all users
-// view: Route<'GET', '/users/:id'> - Shows a single user
-// }
-```
-
-If you want to use a param name other than `id`, you can use the `param` option.
-
-```tsx
-import { createRouter } from 'remix/fetch-router'
-import { resources, route } from 'remix/fetch-router/routes'
-
-let routes = route({
- users: resources('users', {
- only: ['index', 'show', 'edit', 'update'],
- param: 'userId',
- }),
-})
-type Routes = typeof routes.users
-// {
-// index: Route<'GET', '/users'> - Lists all users
-// show: Route<'GET', '/users/:userId'> - Shows a single user
-// edit: Route<'GET', '/users/:userId/edit'> - Shows a form to edit a user
-// update: Route<'PUT', '/users/:userId'> - Updates a user
-// }
-```
-
-You can use the `exclude` option to exclude routes from being generated.
-
-```tsx
-let routes = resources('users', { exclude: ['edit', 'update', 'destroy'] })
-type Routes = typeof routes
-// {
-// index: Route<'GET', '/users'> - Lists all users
-// new: Route<'GET', '/users/new'> - Shows a form to create a new user
-// show: Route<'GET', '/users/:userId'> - Shows a single user
-// create: Route<'POST', '/users'> - Creates a new user
-// }
-```
-
-## Navigation
-
-- [Routing based on request method](./routing-methods.md)
-- [Basic usage and route maps](./usage.md)
-- [fetch-router overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/testing-and-related.md b/docs/agents/remix/fetch-router/testing-and-related.md
deleted file mode 100644
index 16607c5..0000000
--- a/docs/agents/remix/fetch-router/testing-and-related.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Testing and related work
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-## Testing
-
-Testing is straightforward because `fetch-router` uses the standard `fetch()`
-API:
-
-```ts
-import * as assert from 'node:assert/strict'
-import { describe, it } from 'node:test'
-
-describe('blog routes', () => {
- it('creates a new post', async () => {
- let response = await router.fetch('https://api.remix.run/posts', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ title: 'Hello', content: 'World' }),
- })
-
- assert.equal(response.status, 201)
- let post = await response.json()
- assert.equal(post.title, 'Hello')
- })
-
- it('returns 404 for missing posts', async () => {
- let response = await router.fetch('https://api.remix.run/posts/not-found')
- assert.equal(response.status, 404)
- })
-})
-```
-
-No special test harness or mocking required! Just use `fetch()` like you would
-in production.
-
-## Related work
-
-- [@remix-run/response](../response/index.md) - Response helpers for HTML, JSON,
- files, and redirects
-- [@remix-run/headers](../headers/index.md) - A library for working with HTTP
- headers
-- [@remix-run/form-data-parser](../form-data-parser) - A library for parsing
- multipart/form-data requests
-- [@remix-run/route-pattern](../route-pattern) - The pattern matching library
- that powers `fetch-router`
-- [Express](https://expressjs.com/) - The classic Node.js web framework
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/fetch-router/usage.md b/docs/agents/remix/fetch-router/usage.md
deleted file mode 100644
index 14fc7b8..0000000
--- a/docs/agents/remix/fetch-router/usage.md
+++ /dev/null
@@ -1,170 +0,0 @@
-# Basic usage and route maps
-
-Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router
-
-The main purpose of the router is to map incoming requests to request handlers
-and middleware. The router uses the `fetch()` API to accept a
-[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return
-a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
-
-The example below is a small site with a home page, an "about" page, and a blog.
-
-```ts
-import { createRouter } from 'remix/fetch-router'
-import { route } from 'remix/fetch-router/routes'
-import { logger } from 'remix/logger-middleware'
-
-// `route()` creates a "route map" that organizes routes by name. The keys
-// of the map may be any name, and may be nested to group related routes.
-let routes = route({
- home: '/',
- about: '/about',
- blog: {
- index: '/blog',
- show: '/blog/:slug',
- },
-})
-
-let router = createRouter({
- // Middleware may be used to run code before and/or after actions run.
- // In this case, the `logger()` middleware logs the request to the console.
- middleware: [logger()],
-})
-
-// Map the routes to a "controller" that defines actions for each route. The
-// structure of the controller mirrors the structure of the route map.
-router.map(routes, {
- home() {
- return new Response('Home')
- },
- about() {
- return new Response('About')
- },
- blog: {
- index() {
- return new Response('Blog')
- },
- show({ params }) {
- // params is a type-safe object with the parameters from the route pattern
- return new Response(`Post ${params.slug}`)
- },
- },
-})
-
-let response = await router.fetch('https://remix.run/blog/hello-remix')
-console.log(await response.text()) // "Post hello-remix"
-```
-
-The route map is an object of the same shape as the object passed into
-`route()`, including nested objects. The leaves of the map are `Route` objects,
-which you can see if you inspect the type of the `routes` variable in your IDE.
-
-```ts
-type Routes = typeof routes
-// {
-// home: Route<'ANY', '/'>
-// about: Route<'ANY', '/about'>
-// blog: {
-// index: Route<'ANY', '/blog'>
-// show: Route<'ANY', '/blog/:slug'>
-// },
-// }
-```
-
-The `routes.home` route is a `Route<'ANY', '/'>`, which means it serves any
-request method (`GET`, `POST`, `PUT`, `DELETE`, etc.) when the URL path is `/`.
-We'll discuss routing based on request method in the routing guide.
-
-## Links and form actions
-
-In addition to describing the structure of your routes, route maps also make it
-easy to generate type-safe links and form actions using the `href()` function on
-a route. The example below is a small site with a home page and a "Contact Us"
-page.
-
-Note: We're using the
-[`createHtmlResponse` helper from `remix/response`](https://github.com/remix-run/remix/tree/main/packages/response/README.md#html-responses)
-below to create `Response`s with `Content-Type: text/html`. We're also using the
-`html` template tag to create safe HTML strings to use in the response body.
-
-```ts
-import { createRouter } from 'remix/fetch-router'
-import { route } from 'remix/fetch-router/routes'
-import { html } from 'remix/html-template'
-import { createHtmlResponse } from 'remix/response/html'
-
-let routes = route({
- home: '/',
- contact: '/contact',
-})
-
-let router = createRouter()
-
-// Register an action for `GET /`
-router.get(routes.home, () => {
- return createHtmlResponse(`
-
-
-
Home
-
- Contact Us
-
-
-
- `)
-})
-
-// Register an action for `GET /contact`
-router.get(routes.contact, () => {
- return createHtmlResponse(`
-
-
-
Contact Us
-
-
-
-
- `)
-})
-
-// Register an action for `POST /contact`
-router.post(routes.contact, ({ formData }) => {
- // POST actions receive a `context` object with a `formData` property that
- // contains the `FormData` from the form submission. It is automatically
- // parsed from the request body and available in all POST actions.
- let message = formData.get('message') as string
- let body = html`
-
-
-
Thanks!
-
-
-
-
- `
-
- return createHtmlResponse(body)
-})
-```
-
-## Navigation
-
-- [fetch-router overview](./index.md)
-- [Routing based on request method](./routing-methods.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/file-storage-s3.md b/docs/agents/remix/file-storage-s3.md
deleted file mode 100644
index ed64e78..0000000
--- a/docs/agents/remix/file-storage-s3.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# file-storage-s3
-
-Source: https://github.com/remix-run/remix/tree/main/packages/file-storage-s3
-
-## README
-
-S3 backend for `remix/file-storage`.
-
-Use this package when you want the `FileStorage` API backed by AWS S3 or an
-S3-compatible provider (MinIO, LocalStack, etc.).
-
-## Installation
-
-```sh
-npm i remix
-```
-
-## Usage
-
-```ts
-import { createS3FileStorage } from 'remix/file-storage-s3'
-
-let storage = createS3FileStorage({
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
- bucket: 'my-app-uploads',
- region: 'us-east-1',
-})
-```
-
-Use `endpoint` and `forcePathStyle: true` for non-AWS S3-compatible providers.
-
-## Related packages
-
-- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage)
-- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser)
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/file-storage-s3/README.md b/docs/agents/remix/file-storage-s3/README.md
new file mode 100644
index 0000000..afad9fb
--- /dev/null
+++ b/docs/agents/remix/file-storage-s3/README.md
@@ -0,0 +1,55 @@
+# file-storage-s3
+
+S3 backend for
+[`remix/file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage).
+Use this package when you want the `FileStorage` API backed by AWS S3 or an
+S3-compatible provider.
+
+## Features
+
+- **S3-Compatible API** - Works with AWS S3 and S3-compatible APIs (e.g. MinIO,
+ LocalStack)
+- **Metadata Preservation** - Preserves `File` metadata (`name`, `type`,
+ `lastModified`)
+- **Runtime-Agnostic Signing** - Uses
+ [`aws4fetch`](https://github.com/mhart/aws4fetch) for SigV4 signing
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Usage
+
+```ts
+import { createS3FileStorage } from 'remix/file-storage-s3'
+
+let storage = createS3FileStorage({
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
+ bucket: 'my-app-uploads',
+ region: 'us-east-1',
+})
+
+await storage.set(
+ 'uploads/hello.txt',
+ new File(['hello world'], 'hello.txt', { type: 'text/plain' }),
+)
+let file = await storage.get('uploads/hello.txt')
+await storage.remove('uploads/hello.txt')
+```
+
+For S3-compatible providers such as MinIO and LocalStack, set `endpoint` and
+`forcePathStyle: true`.
+
+## Related Packages
+
+- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) -
+ Core `FileStorage` interface and filesystem/memory backends
+- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
+ Parses `multipart/form-data` uploads into `FileUpload` objects
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/file-storage.md b/docs/agents/remix/file-storage/README.md
similarity index 62%
rename from docs/agents/remix/file-storage.md
rename to docs/agents/remix/file-storage/README.md
index 4cf3af1..c144d59 100644
--- a/docs/agents/remix/file-storage.md
+++ b/docs/agents/remix/file-storage/README.md
@@ -1,39 +1,22 @@
# file-storage
-Source: https://github.com/remix-run/remix/tree/main/packages/file-storage
-
-## README
-
-`file-storage` is a key/value interface for storing
-[`File` objects](https://developer.mozilla.org/en-US/docs/Web/API/File) in
-JavaScript.
-
-Handling file uploads and storage is a common requirement in web applications,
-but each storage backend (local disk, AWS S3, Cloudflare R2, etc.) has its own
-API and conventions. This fragmentation makes it difficult to write portable
-code that can easily switch between storage providers or support multiple
-backends simultaneously.
-
-Similar to how `localStorage` allows you to store key/value pairs of strings in
-the browser, `file-storage` allows you to store key/value pairs of files on the
-server with a consistent interface regardless of the underlying storage
-mechanism.
+Key/value storage interfaces for server-side
+[`File` objects](https://developer.mozilla.org/en-US/docs/Web/API/File).
+`file-storage` gives Remix apps one consistent API across local disk and memory
+backends.
## Features
- **Simple API** - Intuitive key/value API (like
[Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API),
but for `File`s instead of strings)
-- **Generic Interface** - `FileStorage` interface that works for various large
- object storage backends (can be adapted to AWS S3, Cloudflare R2, etc.)
+- **Multiple Backends** - Built-in filesystem and memory backends
- **Streaming Support** - Stream file content to and from storage
- **Metadata Preservation** - Preserves all `File` metadata including
`file.name`, `file.type`, and `file.lastModified`
## Installation
-Install from [npm](https://www.npmjs.com/):
-
```sh
npm i remix
```
@@ -77,7 +60,3 @@ await storage.remove(key)
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/form-data-middleware.md b/docs/agents/remix/form-data-middleware.md
deleted file mode 100644
index df789f4..0000000
--- a/docs/agents/remix/form-data-middleware.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# form-data-middleware
-
-Source:
-https://github.com/remix-run/remix/tree/main/packages/form-data-middleware
-
-## README
-
-Middleware for parsing
-[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) from
-incoming request bodies for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-## Installation
-
-```sh
-bun add @remix-run/form-data-middleware
-```
-
-## Usage
-
-Use the `formData()` middleware at the router level to parse `FormData` from the
-request body and make it available on the request context as `context.formData`.
-
-`context.files` will also be available as a map of `File` objects keyed by the
-name of the form field.
-
-```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { formData } from '@remix-run/form-data-middleware'
-
-let router = createRouter({
- middleware: [formData()],
-})
-
-router.post('/users', async (context) => {
- let name = context.formData.get('name')
- let email = context.formData.get('email')
-
- // Handle file uploads
- let avatar = context.files?.get('avatar')
-
- return Response.json({ name, email, hasAvatar: !!avatar })
-})
-```
-
-### Custom File Upload Handler
-
-You can use a custom upload handler to customize how file uploads are handled.
-The return value of the upload handler will be used as the value of the form
-field in the `FormData` object.
-
-```ts
-import { formData } from '@remix-run/form-data-middleware'
-import { writeFile } from 'node:fs/promises'
-
-let router = createRouter({
- middleware: [
- formData({
- async uploadHandler(upload) {
- // Save to disk and return path
- let path = `./uploads/${upload.name}`
- await writeFile(path, Buffer.from(await upload.arrayBuffer()))
- return path
- },
- }),
- ],
-})
-```
-
-### Suppress Parse Errors
-
-Some requests may contain invalid form data that cannot be parsed. You can
-suppress parse errors by setting `suppressErrors` to `true`. In these cases,
-`context.formData` will be an empty `FormData` object.
-
-```ts
-let router = createRouter({
- middleware: [
- formData({
- suppressErrors: true, // Invalid form data won't throw
- }),
- ],
-})
-```
-
-## Related Packages
-
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router for the web Fetch API
-- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
- The underlying form data parser
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/form-data-middleware/README.md b/docs/agents/remix/form-data-middleware/README.md
new file mode 100644
index 0000000..89b686f
--- /dev/null
+++ b/docs/agents/remix/form-data-middleware/README.md
@@ -0,0 +1,121 @@
+# form-data-middleware
+
+Form body parsing middleware for Remix. It parses incoming
+[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and
+exposes it via `context.get(FormData)`.
+
+## Features
+
+- **Request Form Parsing** - Parses request body form data once per request
+- **File Access** - Uploaded files are available from `context.get(FormData)`
+- **Custom Upload Handling** - Supports pluggable upload handlers for file
+ processing
+- **Error Control** - Optional suppression for malformed form data
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Usage
+
+Use the `formData()` middleware at the router level to parse `FormData` from the
+request body and make it available on request context via
+`context.get(FormData)`.
+
+Uploaded files are available in the parsed `FormData` object. For a single file
+field, use `formData.get(name)`. For repeated file fields, use
+`formData.getAll(name)`.
+
+```ts
+import { createRouter } from 'remix/fetch-router'
+import { formData } from 'remix/form-data-middleware'
+
+let router = createRouter({
+ middleware: [formData()],
+})
+
+router.post('/users', async (context) => {
+ let formData = context.get(FormData)
+ let name = formData.get('name')
+ let email = formData.get('email')
+
+ // Handle file uploads
+ let avatar = formData.get('avatar')
+
+ return Response.json({ name, email, hasAvatar: avatar instanceof File })
+})
+```
+
+### Custom File Upload Handler
+
+You can use a custom upload handler to customize how file uploads are handled.
+The return value of the upload handler will be used as the value of the form
+field in the `FormData` object.
+
+```ts
+import { formData } from 'remix/form-data-middleware'
+import { writeFile } from 'node:fs/promises'
+
+let router = createRouter({
+ middleware: [
+ formData({
+ async uploadHandler(upload) {
+ // Save to disk and return path
+ let path = `./uploads/${upload.name}`
+ await writeFile(path, Buffer.from(await upload.arrayBuffer()))
+ return path
+ },
+ }),
+ ],
+})
+```
+
+### Limit Multipart Growth
+
+`formData()` forwards multipart limit options to `parseFormData()`, so you can
+cap uploads with `maxHeaderSize`, `maxFiles`, `maxFileSize`, `maxParts`, and
+`maxTotalSize`.
+
+```ts
+let router = createRouter({
+ middleware: [
+ formData({
+ maxFiles: 5,
+ maxFileSize: 10 * 1024 * 1024,
+ maxParts: 25,
+ maxTotalSize: 12 * 1024 * 1024,
+ }),
+ ],
+})
+```
+
+### Suppress Parse Errors
+
+Some requests may contain invalid form data that cannot be parsed. You can
+suppress those malformed-body parse errors by setting `suppressErrors` to
+`true`. In these cases, `context.get(FormData)` will be an empty `FormData`
+object. Multipart limit violations from `maxHeaderSize`, `maxFiles`,
+`maxFileSize`, `maxParts`, or `maxTotalSize` are never suppressed.
+
+```ts
+let router = createRouter({
+ middleware: [
+ formData({
+ suppressErrors: true, // Invalid form data won't throw
+ }),
+ ],
+})
+```
+
+## Related Packages
+
+- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
+ Router for the web Fetch API
+- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
+ The underlying form data parser
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/form-data-parser.md b/docs/agents/remix/form-data-parser/README.md
similarity index 79%
rename from docs/agents/remix/form-data-parser.md
rename to docs/agents/remix/form-data-parser/README.md
index f80095d..7d592cc 100644
--- a/docs/agents/remix/form-data-parser.md
+++ b/docs/agents/remix/form-data-parser/README.md
@@ -1,9 +1,5 @@
# form-data-parser
-Source: https://github.com/remix-run/remix/tree/main/packages/form-data-parser
-
-## README
-
A streaming `multipart/form-data` parser that solves memory issues with file
uploads in server environments. Built as an enhanced replacement for the native
`request.formData()` API, it enables efficient handling of large file uploads by
@@ -49,10 +45,8 @@ request body stream, allowing you to safely store files and use either a) the
## Installation
-Install from [npm](https://www.npmjs.com/):
-
```sh
-bun add @remix-run/form-data-parser
+npm i remix
```
## Usage
@@ -62,8 +56,8 @@ for fine-grained control of handling file uploads.
```ts
import * as fsp from 'node:fs/promises'
-import type { FileUpload } from '@remix-run/form-data-parser'
-import { parseFormData } from '@remix-run/form-data-parser'
+import type { FileUpload } from 'remix/form-data-parser'
+import { parseFormData } from 'remix/form-data-parser'
// Define how to handle incoming file uploads
async function uploadHandler(fileUpload: FileUpload) {
@@ -98,15 +92,22 @@ async function requestHandler(request: Request) {
}
```
-To limit the maximum size of files that are uploaded, or the maximum number of
-files that may be uploaded in a single request, use the `maxFileSize` and
-`maxFiles` options.
+To validate the resulting `FormData` object with `remix/data-schema`, use the
+`remix/data-schema/form-data` helpers.
+
+To limit the overall shape of multipart requests, use the `maxHeaderSize`,
+`maxFileSize`, `maxFiles`, `maxParts`, and `maxTotalSize` options. By default,
+`parseFormData()` uses `maxFiles = 20`, `maxParts = 1000`, and
+`maxTotalSize = maxFiles * maxFileSize + 1 MiB`.
```ts
import {
MaxFilesExceededError,
MaxFileSizeExceededError,
-} from '@remix-run/form-data-parser'
+ MaxHeaderSizeExceededError,
+ MaxPartsExceededError,
+ MaxTotalSizeExceededError,
+} from 'remix/form-data-parser'
const oneKb = 1024
const oneMb = 1024 * oneKb
@@ -115,12 +116,20 @@ try {
let formData = await parseFormData(request, {
maxFiles: 5,
maxFileSize: 10 * oneMb,
+ maxParts: 25,
+ maxTotalSize: 12 * oneMb,
})
} catch (error) {
if (error instanceof MaxFilesExceededError) {
console.error(`Request may not contain more than 5 files`)
+ } else if (error instanceof MaxHeaderSizeExceededError) {
+ console.error(`Multipart headers may not exceed the configured size limit`)
} else if (error instanceof MaxFileSizeExceededError) {
console.error(`Files may not be larger than 10 MiB`)
+ } else if (error instanceof MaxPartsExceededError) {
+ console.error(`Request may not contain more than 25 multipart parts`)
+ } else if (error instanceof MaxTotalSizeExceededError) {
+ console.error(`Multipart request may not exceed 12 MiB of total content`)
} else {
console.error(`An unknown error occurred:`, error)
}
@@ -133,9 +142,9 @@ are uploaded, this library pairs really well with
for keeping files in various storage backends.
```ts
-import { LocalFileStorage } from '@remix-run/file-storage/local'
-import type { FileUpload } from '@remix-run/form-data-parser'
-import { parseFormData } from '@remix-run/form-data-parser'
+import { LocalFileStorage } from 'remix/file-storage/local'
+import type { FileUpload } from 'remix/form-data-parser'
+import { parseFormData } from 'remix/form-data-parser'
// Set up storage for uploaded files
const fileStorage = new LocalFileStorage('/uploads/user-avatars')
@@ -168,6 +177,9 @@ contains working demos:
## Related Packages
+- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) -
+ Tiny, standards-aligned validation with a `form-data` export for `FormData`
+ and `URLSearchParams`
- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) -
A simple key/value interface for storing `FileUpload` objects you get from the
parser
@@ -177,7 +189,3 @@ contains working demos:
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/fs.md b/docs/agents/remix/fs/README.md
similarity index 79%
rename from docs/agents/remix/fs.md
rename to docs/agents/remix/fs/README.md
index 8e5c38b..03d3817 100644
--- a/docs/agents/remix/fs.md
+++ b/docs/agents/remix/fs/README.md
@@ -1,13 +1,7 @@
# fs
-Source: https://github.com/remix-run/remix/tree/main/packages/fs
-
-## README
-
-Lazy, streaming filesystem utilities for JavaScript.
-
-This package provides utilities for working with files on the local filesystem
-using the
+Lazy, streaming filesystem utilities for JavaScript. This package provides
+utilities for working with files on the local filesystem using the
[`LazyFile`](https://github.com/remix-run/remix/tree/main/packages/lazy-file)/
native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) API.
@@ -24,10 +18,8 @@ native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) API.
## Installation
-Install from [npm](https://www.npmjs.com/):
-
```sh
-bun add @remix-run/fs
+npm i remix
```
## Usage
@@ -35,7 +27,7 @@ bun add @remix-run/fs
### Opening Lazy Files
```ts
-import { openLazyFile } from '@remix-run/fs'
+import { openLazyFile } from 'remix/fs'
// Open a file from the filesystem
let lazyFile = openLazyFile('./path/to/file.json')
@@ -54,7 +46,7 @@ let customLazyFile = openLazyFile('./image.jpg', {
### Writing Files
```ts
-import { openLazyFile, writeFile } from '@remix-run/fs'
+import { openLazyFile, writeFile } from 'remix/fs'
// Read a file and write it elsewhere
let lazyFile = openLazyFile('./source.txt')
@@ -77,7 +69,3 @@ await handle.close()
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/headers/README.md b/docs/agents/remix/headers/README.md
new file mode 100644
index 0000000..0be7b37
--- /dev/null
+++ b/docs/agents/remix/headers/README.md
@@ -0,0 +1,618 @@
+# headers
+
+Typed utilities for parsing, manipulating, and serializing HTTP header values.
+`headers` provides focused classes for common HTTP headers.
+
+## Features
+
+- **Header-Specific Classes** - Purpose-built APIs for `Accept`,
+ `Cache-Control`, `Content-Type`, and more
+- **Round-Trip Safety** - Parse from raw values and serialize back with
+ `.toString()`
+- **Typed Operations** - Work with structured values instead of manual string
+ parsing
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Individual Header Utilities
+
+Each supported header has a class that represents the header value. Use the
+static `from()` method to parse header values. Each class has a `toString()`
+method that returns the header value as a string, which you can either call
+manually, or will be called automatically when the header class is used in a
+context that expects a string.
+
+The following headers are currently supported:
+
+- [Accept](./README.md#accept)
+- [Accept-Encoding](./README.md#accept-encoding)
+- [Accept-Language](./README.md#accept-language)
+- [Cache-Control](./README.md#cache-control)
+- [Content-Disposition](./README.md#content-disposition)
+- [Content-Range](./README.md#content-range)
+- [Content-Type](./README.md#content-type)
+- [Cookie](./README.md#cookie)
+- [If-Match](./README.md#if-match)
+- [If-None-Match](./README.md#if-none-match)
+- [If-Range](./README.md#if-range)
+- [Range](./README.md#range)
+- [Set-Cookie](./README.md#set-cookie)
+- [Vary](./README.md#vary)
+
+### Accept
+
+Parse, manipulate and stringify
+[`Accept` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept).
+
+Implements `Map
`.
+
+```ts
+import { Accept } from 'remix/headers'
+
+// Parse from headers
+let accept = Accept.from(request.headers.get('accept'))
+
+accept.mediaTypes // ['text/html', 'text/*']
+accept.weights // [1, 0.9]
+accept.accepts('text/html') // true
+accept.accepts('text/plain') // true (matches text/*)
+accept.accepts('image/jpeg') // false
+accept.getWeight('text/plain') // 1 (matches text/*)
+accept.getPreferred(['text/html', 'text/plain']) // 'text/html'
+
+// Iterate
+for (let [mediaType, quality] of accept) {
+ // ...
+}
+
+// Modify and set header
+accept.set('application/json', 0.8)
+accept.delete('text/*')
+headers.set('Accept', accept)
+
+// Construct directly
+new Accept('text/html, text/*;q=0.9')
+new Accept({ 'text/html': 1, 'text/*': 0.9 })
+new Accept(['text/html', ['text/*', 0.9]])
+
+// Use class for type safety when setting Headers values
+// via Accept's `.toString()` method
+let headers = new Headers({
+ Accept: new Accept({ 'text/html': 1, 'application/json': 0.8 }),
+})
+headers.set('Accept', new Accept({ 'text/html': 1, 'application/json': 0.8 }))
+```
+
+### Accept-Encoding
+
+Parse, manipulate and stringify
+[`Accept-Encoding` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding).
+
+Implements `Map`.
+
+```ts
+import { AcceptEncoding } from 'remix/headers'
+
+// Parse from headers
+let acceptEncoding = AcceptEncoding.from(request.headers.get('accept-encoding'))
+
+acceptEncoding.encodings // ['gzip', 'deflate']
+acceptEncoding.weights // [1, 0.8]
+acceptEncoding.accepts('gzip') // true
+acceptEncoding.accepts('br') // false
+acceptEncoding.getWeight('gzip') // 1
+acceptEncoding.getPreferred(['gzip', 'deflate', 'br']) // 'gzip'
+
+// Modify and set header
+acceptEncoding.set('br', 1)
+acceptEncoding.delete('deflate')
+headers.set('Accept-Encoding', acceptEncoding)
+
+// Construct directly
+new AcceptEncoding('gzip, deflate;q=0.8')
+new AcceptEncoding({ gzip: 1, deflate: 0.8 })
+
+// Use class for type safety when setting Headers values
+// via AcceptEncoding's `.toString()` method
+let headers = new Headers({
+ 'Accept-Encoding': new AcceptEncoding({ gzip: 1, br: 0.9 }),
+})
+headers.set('Accept-Encoding', new AcceptEncoding({ gzip: 1, br: 0.9 }))
+```
+
+### Accept-Language
+
+Parse, manipulate and stringify
+[`Accept-Language` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language).
+
+Implements `Map`.
+
+```ts
+import { AcceptLanguage } from 'remix/headers'
+
+// Parse from headers
+let acceptLanguage = AcceptLanguage.from(request.headers.get('accept-language'))
+
+acceptLanguage.languages // ['en-us', 'en']
+acceptLanguage.weights // [1, 0.9]
+acceptLanguage.accepts('en-US') // true
+acceptLanguage.accepts('en-GB') // true (matches en)
+acceptLanguage.getWeight('en-GB') // 1 (matches en)
+acceptLanguage.getPreferred(['en-US', 'en-GB', 'fr']) // 'en-US'
+
+// Modify and set header
+acceptLanguage.set('fr', 0.5)
+acceptLanguage.delete('en')
+headers.set('Accept-Language', acceptLanguage)
+
+// Construct directly
+new AcceptLanguage('en-US, en;q=0.9')
+new AcceptLanguage({ 'en-US': 1, en: 0.9 })
+
+// Use class for type safety when setting Headers values
+// via AcceptLanguage's `.toString()` method
+let headers = new Headers({
+ 'Accept-Language': new AcceptLanguage({ 'en-US': 1, fr: 0.5 }),
+})
+headers.set('Accept-Language', new AcceptLanguage({ 'en-US': 1, fr: 0.5 }))
+```
+
+### Cache-Control
+
+Parse, manipulate and stringify
+[`Cache-Control` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
+
+```ts
+import { CacheControl } from 'remix/headers'
+
+// Parse from headers
+let cacheControl = CacheControl.from(response.headers.get('cache-control'))
+
+cacheControl.public // true
+cacheControl.maxAge // 3600
+cacheControl.sMaxage // 7200
+cacheControl.noCache // undefined
+cacheControl.noStore // undefined
+cacheControl.noTransform // undefined
+cacheControl.mustRevalidate // undefined
+cacheControl.immutable // undefined
+
+// Modify and set header
+cacheControl.maxAge = 7200
+cacheControl.immutable = true
+headers.set('Cache-Control', cacheControl)
+
+// Construct directly
+new CacheControl('public, max-age=3600')
+new CacheControl({ public: true, maxAge: 3600 })
+
+// Use class for type safety when setting Headers values
+// via CacheControl's `.toString()` method
+let headers = new Headers({
+ 'Cache-Control': new CacheControl({ public: true, maxAge: 3600 }),
+})
+headers.set('Cache-Control', new CacheControl({ public: true, maxAge: 3600 }))
+```
+
+### Content-Disposition
+
+Parse, manipulate and stringify
+[`Content-Disposition` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition).
+
+```ts
+import { ContentDisposition } from 'remix/headers'
+
+// Parse from headers
+let contentDisposition = ContentDisposition.from(
+ response.headers.get('content-disposition'),
+)
+
+contentDisposition.type // 'attachment'
+contentDisposition.filename // 'example.pdf'
+contentDisposition.filenameSplat // "UTF-8''%E4%BE%8B%E5%AD%90.pdf"
+contentDisposition.preferredFilename // '例子.pdf' (decoded from filename*)
+
+// Modify and set header
+contentDisposition.filename = 'download.pdf'
+headers.set('Content-Disposition', contentDisposition)
+
+// Construct directly
+new ContentDisposition('attachment; filename="example.pdf"')
+new ContentDisposition({ type: 'attachment', filename: 'example.pdf' })
+
+// Use class for type safety when setting Headers values
+// via ContentDisposition's `.toString()` method
+let headers = new Headers({
+ 'Content-Disposition': new ContentDisposition({
+ type: 'attachment',
+ filename: 'example.pdf',
+ }),
+})
+headers.set(
+ 'Content-Disposition',
+ new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }),
+)
+```
+
+### Content-Range
+
+Parse, manipulate and stringify
+[`Content-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range).
+
+```ts
+import { ContentRange } from 'remix/headers'
+
+// Parse from headers
+let contentRange = ContentRange.from(response.headers.get('content-range'))
+
+contentRange.unit // "bytes"
+contentRange.start // 200
+contentRange.end // 1000
+contentRange.size // 67589
+
+// Unsatisfied range
+let unsatisfied = ContentRange.from('bytes */67589')
+unsatisfied.start // null
+unsatisfied.end // null
+unsatisfied.size // 67589
+
+// Construct directly
+new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 })
+
+// Use class for type safety when setting Headers values
+// via ContentRange's `.toString()` method
+let headers = new Headers({
+ 'Content-Range': new ContentRange({
+ unit: 'bytes',
+ start: 0,
+ end: 499,
+ size: 1000,
+ }),
+})
+headers.set(
+ 'Content-Range',
+ new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }),
+)
+```
+
+### Content-Type
+
+Parse, manipulate and stringify
+[`Content-Type` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type).
+
+```ts
+import { ContentType } from 'remix/headers'
+
+// Parse from headers
+let contentType = ContentType.from(request.headers.get('content-type'))
+
+contentType.mediaType // "text/html"
+contentType.charset // "utf-8"
+contentType.boundary // undefined (or boundary string for multipart)
+
+// Modify and set header
+contentType.charset = 'iso-8859-1'
+headers.set('Content-Type', contentType)
+
+// Construct directly
+new ContentType('text/html; charset=utf-8')
+new ContentType({ mediaType: 'text/html', charset: 'utf-8' })
+
+// Use class for type safety when setting Headers values
+// via ContentType's `.toString()` method
+let headers = new Headers({
+ 'Content-Type': new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),
+})
+headers.set(
+ 'Content-Type',
+ new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),
+)
+```
+
+### Cookie
+
+Parse, manipulate and stringify
+[`Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie).
+
+Implements `Map`.
+
+```ts
+import { Cookie } from 'remix/headers'
+
+// Parse from headers
+let cookie = Cookie.from(request.headers.get('cookie'))
+
+cookie.get('session_id') // 'abc123'
+cookie.get('theme') // 'dark'
+cookie.has('session_id') // true
+cookie.size // 2
+
+// Iterate
+for (let [name, value] of cookie) {
+ // ...
+}
+
+// Modify and set header
+cookie.set('theme', 'light')
+cookie.delete('session_id')
+headers.set('Cookie', cookie)
+
+// Construct directly
+new Cookie('session_id=abc123; theme=dark')
+new Cookie({ session_id: 'abc123', theme: 'dark' })
+new Cookie([
+ ['session_id', 'abc123'],
+ ['theme', 'dark'],
+])
+
+// Use class for type safety when setting Headers values
+// via Cookie's `.toString()` method
+let headers = new Headers({
+ Cookie: new Cookie({ session_id: 'abc123', theme: 'dark' }),
+})
+headers.set('Cookie', new Cookie({ session_id: 'abc123', theme: 'dark' }))
+```
+
+### If-Match
+
+Parse, manipulate and stringify
+[`If-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match).
+
+Implements `Set`.
+
+```ts
+import { IfMatch } from 'remix/headers'
+
+// Parse from headers
+let ifMatch = IfMatch.from(request.headers.get('if-match'))
+
+ifMatch.tags // ['"67ab43"', '"54ed21"']
+ifMatch.has('"67ab43"') // true
+ifMatch.matches('"67ab43"') // true (checks precondition)
+ifMatch.matches('"abc123"') // false
+
+// Note: Uses strong comparison only (weak ETags never match)
+let weak = IfMatch.from('W/"67ab43"')
+weak.matches('W/"67ab43"') // false
+
+// Modify and set header
+ifMatch.add('"newetag"')
+ifMatch.delete('"67ab43"')
+headers.set('If-Match', ifMatch)
+
+// Construct directly
+new IfMatch(['abc123', 'def456'])
+
+// Use class for type safety when setting Headers values
+// via IfMatch's `.toString()` method
+let headers = new Headers({
+ 'If-Match': new IfMatch(['"abc123"', '"def456"']),
+})
+headers.set('If-Match', new IfMatch(['"abc123"', '"def456"']))
+```
+
+### If-None-Match
+
+Parse, manipulate and stringify
+[`If-None-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match).
+
+Implements `Set`.
+
+```ts
+import { IfNoneMatch } from 'remix/headers'
+
+// Parse from headers
+let ifNoneMatch = IfNoneMatch.from(request.headers.get('if-none-match'))
+
+ifNoneMatch.tags // ['"67ab43"', '"54ed21"']
+ifNoneMatch.has('"67ab43"') // true
+ifNoneMatch.matches('"67ab43"') // true
+
+// Supports weak comparison (unlike If-Match)
+let weak = IfNoneMatch.from('W/"67ab43"')
+weak.matches('W/"67ab43"') // true
+
+// Modify and set header
+ifNoneMatch.add('"newetag"')
+ifNoneMatch.delete('"67ab43"')
+headers.set('If-None-Match', ifNoneMatch)
+
+// Construct directly
+new IfNoneMatch(['abc123'])
+
+// Use class for type safety when setting Headers values
+// via IfNoneMatch's `.toString()` method
+let headers = new Headers({
+ 'If-None-Match': new IfNoneMatch(['"abc123"']),
+})
+headers.set('If-None-Match', new IfNoneMatch(['"abc123"']))
+```
+
+### If-Range
+
+Parse, manipulate and stringify
+[`If-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range).
+
+```ts
+import { IfRange } from 'remix/headers'
+
+// Parse from headers
+let ifRange = IfRange.from(request.headers.get('if-range'))
+
+// With HTTP date
+ifRange.matches({ lastModified: 1609459200000 }) // true
+ifRange.matches({ lastModified: new Date('2021-01-01') }) // true
+
+// With ETag
+let etagHeader = IfRange.from('"67ab43"')
+etagHeader.matches({ etag: '"67ab43"' }) // true
+
+// Empty/null returns empty instance (range proceeds unconditionally)
+let empty = IfRange.from(null)
+empty.matches({ etag: '"any"' }) // true
+
+// Construct directly
+new IfRange('"abc123"')
+
+// Use class for type safety when setting Headers values
+// via IfRange's `.toString()` method
+let headers = new Headers({
+ 'If-Range': new IfRange('"abc123"'),
+})
+headers.set('If-Range', new IfRange('"abc123"'))
+```
+
+### Range
+
+Parse, manipulate and stringify
+[`Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range).
+
+```ts
+import { Range } from 'remix/headers'
+
+// Parse from headers
+let range = Range.from(request.headers.get('range'))
+
+range.unit // "bytes"
+range.ranges // [{ start: 200, end: 1000 }]
+range.canSatisfy(2000) // true
+range.canSatisfy(500) // false
+range.normalize(2000) // [{ start: 200, end: 1000 }]
+
+// Multiple ranges
+let multi = Range.from('bytes=0-499, 1000-1499')
+multi.ranges.length // 2
+
+// Suffix range (last N bytes)
+let suffix = Range.from('bytes=-500')
+suffix.normalize(2000) // [{ start: 1500, end: 1999 }]
+
+// Construct directly
+new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })
+
+// Use class for type safety when setting Headers values
+// via Range's `.toString()` method
+let headers = new Headers({
+ Range: new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }),
+})
+headers.set(
+ 'Range',
+ new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }),
+)
+```
+
+### Set-Cookie
+
+Parse, manipulate and stringify
+[`Set-Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
+
+```ts
+import { SetCookie } from 'remix/headers'
+
+// Parse from headers
+let setCookie = SetCookie.from(response.headers.get('set-cookie'))
+
+setCookie.name // "session_id"
+setCookie.value // "abc"
+setCookie.path // "/"
+setCookie.httpOnly // true
+setCookie.secure // true
+setCookie.domain // undefined
+setCookie.maxAge // undefined
+setCookie.expires // undefined
+setCookie.sameSite // undefined
+
+// Modify and set header
+setCookie.maxAge = 3600
+setCookie.sameSite = 'Strict'
+headers.set('Set-Cookie', setCookie)
+
+// Construct directly
+new SetCookie('session_id=abc; Path=/; HttpOnly; Secure')
+new SetCookie({
+ name: 'session_id',
+ value: 'abc',
+ path: '/',
+ httpOnly: true,
+ secure: true,
+})
+
+// Use class for type safety when setting Headers values
+// via SetCookie's `.toString()` method
+let headers = new Headers({
+ 'Set-Cookie': new SetCookie({
+ name: 'session_id',
+ value: 'abc',
+ httpOnly: true,
+ }),
+})
+headers.set(
+ 'Set-Cookie',
+ new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }),
+)
+```
+
+### Vary
+
+Parse, manipulate and stringify
+[`Vary` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary).
+
+Implements `Set`.
+
+```ts
+import { Vary } from 'remix/headers'
+
+// Parse from headers
+let vary = Vary.from(response.headers.get('vary'))
+
+vary.headerNames // ['accept-encoding', 'accept-language']
+vary.has('Accept-Encoding') // true (case-insensitive)
+vary.size // 2
+
+// Modify and set header
+vary.add('User-Agent')
+vary.delete('Accept-Language')
+headers.set('Vary', vary)
+
+// Construct directly
+new Vary('Accept-Encoding, Accept-Language')
+new Vary(['Accept-Encoding', 'Accept-Language'])
+new Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })
+
+// Use class for type safety when setting Headers values
+// via Vary's `.toString()` method
+let headers = new Headers({
+ Vary: new Vary(['Accept-Encoding', 'Accept-Language']),
+})
+headers.set('Vary', new Vary(['Accept-Encoding', 'Accept-Language']))
+```
+
+## Raw Headers
+
+Parse and stringify raw HTTP header strings.
+
+```ts
+import { parse, stringify } from 'remix/headers'
+
+let headers = parse('Content-Type: text/html\r\nCache-Control: no-cache')
+headers.get('content-type') // 'text/html'
+headers.get('cache-control') // 'no-cache'
+
+stringify(headers)
+// 'Content-Type: text/html\r\nCache-Control: no-cache'
+```
+
+## Related Packages
+
+- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) -
+ Build HTTP proxy servers using the web fetch API
+- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) -
+ Build HTTP servers on Node.js using the web fetch API
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/headers/accept-headers.md b/docs/agents/remix/headers/accept-headers.md
deleted file mode 100644
index bf2b9a0..0000000
--- a/docs/agents/remix/headers/accept-headers.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# Accept headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-Each supported header has a class that represents the header value. Use the
-static `from()` method to parse header values. Each class has a `toString()`
-method that returns the header value as a string.
-
-## Accept
-
-Parse, manipulate and stringify
-[`Accept` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept).
-
-Implements `Map`.
-
-```ts
-import { Accept } from '@remix-run/headers'
-
-// Parse from headers
-let accept = Accept.from(request.headers.get('accept'))
-
-accept.mediaTypes // ['text/html', 'text/*']
-accept.weights // [1, 0.9]
-accept.accepts('text/html') // true
-accept.accepts('text/plain') // true (matches text/*)
-accept.accepts('image/jpeg') // false
-accept.getWeight('text/plain') // 1 (matches text/*)
-accept.getPreferred(['text/html', 'text/plain']) // 'text/html'
-
-// Iterate
-for (let [mediaType, quality] of accept) {
- // ...
-}
-
-// Modify and set header
-accept.set('application/json', 0.8)
-accept.delete('text/*')
-headers.set('Accept', accept)
-
-// Construct directly
-new Accept('text/html, text/*;q=0.9')
-new Accept({ 'text/html': 1, 'text/*': 0.9 })
-new Accept(['text/html', ['text/*', 0.9]])
-
-// Use class for type safety when setting Headers values
-// via Accept's `.toString()` method
-let headers = new Headers({
- Accept: new Accept({ 'text/html': 1, 'application/json': 0.8 }),
-})
-headers.set('Accept', new Accept({ 'text/html': 1, 'application/json': 0.8 }))
-```
-
-## Accept-Encoding
-
-Parse, manipulate and stringify
-[`Accept-Encoding` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding).
-
-Implements `Map`.
-
-```ts
-import { AcceptEncoding } from '@remix-run/headers'
-
-// Parse from headers
-let acceptEncoding = AcceptEncoding.from(request.headers.get('accept-encoding'))
-
-acceptEncoding.encodings // ['gzip', 'deflate']
-acceptEncoding.weights // [1, 0.8]
-acceptEncoding.accepts('gzip') // true
-acceptEncoding.accepts('br') // false
-acceptEncoding.getWeight('gzip') // 1
-acceptEncoding.getPreferred(['gzip', 'deflate', 'br']) // 'gzip'
-
-// Modify and set header
-acceptEncoding.set('br', 1)
-acceptEncoding.delete('deflate')
-headers.set('Accept-Encoding', acceptEncoding)
-
-// Construct directly
-new AcceptEncoding('gzip, deflate;q=0.8')
-new AcceptEncoding({ gzip: 1, deflate: 0.8 })
-
-// Use class for type safety when setting Headers values
-// via AcceptEncoding's `.toString()` method
-let headers = new Headers({
- 'Accept-Encoding': new AcceptEncoding({ gzip: 1, br: 0.9 }),
-})
-headers.set('Accept-Encoding', new AcceptEncoding({ gzip: 1, br: 0.9 }))
-```
-
-## Accept-Language
-
-Parse, manipulate and stringify
-[`Accept-Language` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language).
-
-Implements `Map`.
-
-```ts
-import { AcceptLanguage } from '@remix-run/headers'
-
-// Parse from headers
-let acceptLanguage = AcceptLanguage.from(request.headers.get('accept-language'))
-
-acceptLanguage.languages // ['en-us', 'en']
-acceptLanguage.weights // [1, 0.9]
-acceptLanguage.accepts('en-US') // true
-acceptLanguage.accepts('en-GB') // true (matches en)
-acceptLanguage.getWeight('en-GB') // 1 (matches en)
-acceptLanguage.getPreferred(['en-US', 'en-GB', 'fr']) // 'en-US'
-
-// Modify and set header
-acceptLanguage.set('fr', 0.5)
-acceptLanguage.delete('en')
-headers.set('Accept-Language', acceptLanguage)
-
-// Construct directly
-new AcceptLanguage('en-US, en;q=0.9')
-new AcceptLanguage({ 'en-US': 1, en: 0.9 })
-
-// Use class for type safety when setting Headers values
-// via AcceptLanguage's `.toString()` method
-let headers = new Headers({
- 'Accept-Language': new AcceptLanguage({ 'en-US': 1, fr: 0.5 }),
-})
-headers.set('Accept-Language', new AcceptLanguage({ 'en-US': 1, fr: 0.5 }))
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Content and cache headers](./content-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/conditional-headers.md b/docs/agents/remix/headers/conditional-headers.md
deleted file mode 100644
index 1dd4cbf..0000000
--- a/docs/agents/remix/headers/conditional-headers.md
+++ /dev/null
@@ -1,192 +0,0 @@
-# Conditionals and ranges
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## If-Match
-
-Parse, manipulate and stringify
-[`If-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match).
-
-Implements `Set`.
-
-```ts
-import { IfMatch } from '@remix-run/headers'
-
-// Parse from headers
-let ifMatch = IfMatch.from(request.headers.get('if-match'))
-
-ifMatch.tags // ['"67ab43"', '"54ed21"']
-ifMatch.has('"67ab43"') // true
-ifMatch.matches('"67ab43"') // true (checks precondition)
-ifMatch.matches('"abc123"') // false
-
-// Note: Uses strong comparison only (weak ETags never match)
-let weak = IfMatch.from('W/"67ab43"')
-weak.matches('W/"67ab43"') // false
-
-// Modify and set header
-ifMatch.add('"newetag"')
-ifMatch.delete('"67ab43"')
-headers.set('If-Match', ifMatch)
-
-// Construct directly
-new IfMatch(['abc123', 'def456'])
-
-// Use class for type safety when setting Headers values
-// via IfMatch's `.toString()` method
-let headers = new Headers({
- 'If-Match': new IfMatch(['"abc123"', '"def456"']),
-})
-headers.set('If-Match', new IfMatch(['"abc123"', '"def456"']))
-```
-
-## If-None-Match
-
-Parse, manipulate and stringify
-[`If-None-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match).
-
-Implements `Set`.
-
-```ts
-import { IfNoneMatch } from '@remix-run/headers'
-
-// Parse from headers
-let ifNoneMatch = IfNoneMatch.from(request.headers.get('if-none-match'))
-
-ifNoneMatch.tags // ['"67ab43"', '"54ed21"']
-ifNoneMatch.has('"67ab43"') // true
-ifNoneMatch.matches('"67ab43"') // true
-
-// Supports weak comparison (unlike If-Match)
-let weak = IfNoneMatch.from('W/"67ab43"')
-weak.matches('W/"67ab43"') // true
-
-// Modify and set header
-ifNoneMatch.add('"newetag"')
-ifNoneMatch.delete('"67ab43"')
-headers.set('If-None-Match', ifNoneMatch)
-
-// Construct directly
-new IfNoneMatch(['abc123'])
-
-// Use class for type safety when setting Headers values
-// via IfNoneMatch's `.toString()` method
-let headers = new Headers({
- 'If-None-Match': new IfNoneMatch(['"abc123"']),
-})
-headers.set('If-None-Match', new IfNoneMatch(['"abc123"']))
-```
-
-## If-Range
-
-Parse, manipulate and stringify
-[`If-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range).
-
-```ts
-import { IfRange } from '@remix-run/headers'
-
-// Parse from headers
-let ifRange = IfRange.from(request.headers.get('if-range'))
-
-// With HTTP date
-ifRange.matches({ lastModified: 1609459200000 }) // true
-ifRange.matches({ lastModified: new Date('2021-01-01') }) // true
-
-// With ETag
-let etagHeader = IfRange.from('"67ab43"')
-etagHeader.matches({ etag: '"67ab43"' }) // true
-
-// Empty/null returns empty instance (range proceeds unconditionally)
-let empty = IfRange.from(null)
-empty.matches({ etag: '"any"' }) // true
-
-// Construct directly
-new IfRange('"abc123"')
-
-// Use class for type safety when setting Headers values
-// via IfRange's `.toString()` method
-let headers = new Headers({
- 'If-Range': new IfRange('"abc123"'),
-})
-headers.set('If-Range', new IfRange('"abc123"'))
-```
-
-## Range
-
-Parse, manipulate and stringify
-[`Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range).
-
-```ts
-import { Range } from '@remix-run/headers'
-
-// Parse from headers
-let range = Range.from(request.headers.get('range'))
-
-range.unit // "bytes"
-range.ranges // [{ start: 200, end: 1000 }]
-range.canSatisfy(2000) // true
-range.canSatisfy(500) // false
-range.normalize(2000) // [{ start: 200, end: 1000 }]
-
-// Multiple ranges
-let multi = Range.from('bytes=0-499, 1000-1499')
-multi.ranges.length // 2
-
-// Suffix range (last N bytes)
-let suffix = Range.from('bytes=-500')
-suffix.normalize(2000) // [{ start: 1500, end: 1999 }]
-
-// Construct directly
-new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })
-
-// Use class for type safety when setting Headers values
-// via Range's `.toString()` method
-let headers = new Headers({
- Range: new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }),
-})
-headers.set(
- 'Range',
- new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }),
-)
-```
-
-## Vary
-
-Parse, manipulate and stringify
-[`Vary` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary).
-
-Implements `Set`.
-
-```ts
-import { Vary } from '@remix-run/headers'
-
-// Parse from headers
-let vary = Vary.from(response.headers.get('vary'))
-
-vary.headerNames // ['accept-encoding', 'accept-language']
-vary.has('Accept-Encoding') // true (case-insensitive)
-vary.size // 2
-
-// Modify and set header
-vary.add('User-Agent')
-vary.delete('Accept-Language')
-headers.set('Vary', vary)
-
-// Construct directly
-new Vary('Accept-Encoding, Accept-Language')
-new Vary(['Accept-Encoding', 'Accept-Language'])
-new Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })
-
-// Use class for type safety when setting Headers values
-// via Vary's `.toString()` method
-let headers = new Headers({
- Vary: new Vary(['Accept-Encoding', 'Accept-Language']),
-})
-headers.set('Vary', new Vary(['Accept-Encoding', 'Accept-Language']))
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Cookie headers](./cookie-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/content-headers.md b/docs/agents/remix/headers/content-headers.md
deleted file mode 100644
index f007953..0000000
--- a/docs/agents/remix/headers/content-headers.md
+++ /dev/null
@@ -1,161 +0,0 @@
-# Content and cache headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Cache-Control
-
-Parse, manipulate and stringify
-[`Cache-Control` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
-
-```ts
-import { CacheControl } from '@remix-run/headers'
-
-// Parse from headers
-let cacheControl = CacheControl.from(response.headers.get('cache-control'))
-
-cacheControl.public // true
-cacheControl.maxAge // 3600
-cacheControl.sMaxage // 7200
-cacheControl.noCache // undefined
-cacheControl.noStore // undefined
-cacheControl.noTransform // undefined
-cacheControl.mustRevalidate // undefined
-cacheControl.immutable // undefined
-
-// Modify and set header
-cacheControl.maxAge = 7200
-cacheControl.immutable = true
-headers.set('Cache-Control', cacheControl)
-
-// Construct directly
-new CacheControl('public, max-age=3600')
-new CacheControl({ public: true, maxAge: 3600 })
-
-// Use class for type safety when setting Headers values
-// via CacheControl's `.toString()` method
-let headers = new Headers({
- 'Cache-Control': new CacheControl({ public: true, maxAge: 3600 }),
-})
-headers.set('Cache-Control', new CacheControl({ public: true, maxAge: 3600 }))
-```
-
-## Content-Disposition
-
-Parse, manipulate and stringify
-[`Content-Disposition` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition).
-
-```ts
-import { ContentDisposition } from '@remix-run/headers'
-
-// Parse from headers
-let contentDisposition = ContentDisposition.from(
- response.headers.get('content-disposition'),
-)
-
-contentDisposition.type // 'attachment'
-contentDisposition.filename // 'example.pdf'
-contentDisposition.filenameSplat // "UTF-8''example.pdf"
-contentDisposition.preferredFilename // 'example.pdf' (decoded from filename*)
-
-// Modify and set header
-contentDisposition.filename = 'download.pdf'
-headers.set('Content-Disposition', contentDisposition)
-
-// Construct directly
-new ContentDisposition('attachment; filename="example.pdf"')
-new ContentDisposition({ type: 'attachment', filename: 'example.pdf' })
-
-// Use class for type safety when setting Headers values
-// via ContentDisposition's `.toString()` method
-let headers = new Headers({
- 'Content-Disposition': new ContentDisposition({
- type: 'attachment',
- filename: 'example.pdf',
- }),
-})
-headers.set(
- 'Content-Disposition',
- new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }),
-)
-```
-
-## Content-Range
-
-Parse, manipulate and stringify
-[`Content-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range).
-
-```ts
-import { ContentRange } from '@remix-run/headers'
-
-// Parse from headers
-let contentRange = ContentRange.from(response.headers.get('content-range'))
-
-contentRange.unit // "bytes"
-contentRange.start // 200
-contentRange.end // 1000
-contentRange.size // 67589
-
-// Unsatisfied range
-let unsatisfied = ContentRange.from('bytes */67589')
-unsatisfied.start // null
-unsatisfied.end // null
-unsatisfied.size // 67589
-
-// Construct directly
-new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 })
-
-// Use class for type safety when setting Headers values
-// via ContentRange's `.toString()` method
-let headers = new Headers({
- 'Content-Range': new ContentRange({
- unit: 'bytes',
- start: 0,
- end: 499,
- size: 1000,
- }),
-})
-headers.set(
- 'Content-Range',
- new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }),
-)
-```
-
-## Content-Type
-
-Parse, manipulate and stringify
-[`Content-Type` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type).
-
-```ts
-import { ContentType } from '@remix-run/headers'
-
-// Parse from headers
-let contentType = ContentType.from(request.headers.get('content-type'))
-
-contentType.mediaType // "text/html"
-contentType.charset // "utf-8"
-contentType.boundary // undefined (or boundary string for multipart)
-
-// Modify and set header
-contentType.charset = 'iso-8859-1'
-headers.set('Content-Type', contentType)
-
-// Construct directly
-new ContentType('text/html; charset=utf-8')
-new ContentType({ mediaType: 'text/html', charset: 'utf-8' })
-
-// Use class for type safety when setting Headers values
-// via ContentType's `.toString()` method
-let headers = new Headers({
- 'Content-Type': new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),
-})
-headers.set(
- 'Content-Type',
- new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),
-)
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Accept headers](./accept-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/cookie-headers.md b/docs/agents/remix/headers/cookie-headers.md
deleted file mode 100644
index 65e4fa3..0000000
--- a/docs/agents/remix/headers/cookie-headers.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Cookie headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Cookie
-
-Parse, manipulate and stringify
-[`Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie).
-
-Implements `Map`.
-
-```ts
-import { Cookie } from '@remix-run/headers'
-
-// Parse from headers
-let cookie = Cookie.from(request.headers.get('cookie'))
-
-cookie.get('session_id') // 'abc123'
-cookie.get('theme') // 'dark'
-cookie.has('session_id') // true
-cookie.size // 2
-
-// Iterate
-for (let [name, value] of cookie) {
- // ...
-}
-
-// Modify and set header
-cookie.set('theme', 'light')
-cookie.delete('session_id')
-headers.set('Cookie', cookie)
-
-// Construct directly
-new Cookie('session_id=abc123; theme=dark')
-new Cookie({ session_id: 'abc123', theme: 'dark' })
-new Cookie([
- ['session_id', 'abc123'],
- ['theme', 'dark'],
-])
-
-// Use class for type safety when setting Headers values
-// via Cookie's `.toString()` method
-let headers = new Headers({
- Cookie: new Cookie({ session_id: 'abc123', theme: 'dark' }),
-})
-headers.set('Cookie', new Cookie({ session_id: 'abc123', theme: 'dark' }))
-```
-
-## Set-Cookie
-
-Parse, manipulate and stringify
-[`Set-Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
-
-```ts
-import { SetCookie } from '@remix-run/headers'
-
-// Parse from headers
-let setCookie = SetCookie.from(response.headers.get('set-cookie'))
-
-setCookie.name // "session_id"
-setCookie.value // "abc"
-setCookie.path // "/"
-setCookie.httpOnly // true
-setCookie.secure // true
-setCookie.domain // undefined
-setCookie.maxAge // undefined
-setCookie.expires // undefined
-setCookie.sameSite // undefined
-
-// Modify and set header
-setCookie.maxAge = 3600
-setCookie.sameSite = 'Strict'
-headers.set('Set-Cookie', setCookie)
-
-// Construct directly
-new SetCookie('session_id=abc; Path=/; HttpOnly; Secure')
-new SetCookie({
- name: 'session_id',
- value: 'abc',
- path: '/',
- httpOnly: true,
- secure: true,
-})
-
-// Use class for type safety when setting Headers values
-// via SetCookie's `.toString()` method
-let headers = new Headers({
- 'Set-Cookie': new SetCookie({
- name: 'session_id',
- value: 'abc',
- httpOnly: true,
- }),
-})
-headers.set(
- 'Set-Cookie',
- new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }),
-)
-```
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Conditionals and ranges](./conditional-headers.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/index.md b/docs/agents/remix/headers/index.md
deleted file mode 100644
index ad5586d..0000000
--- a/docs/agents/remix/headers/index.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# headers
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Overview
-
-Utilities for parsing, manipulating and stringifying HTTP header values.
-
-HTTP headers contain critical information - from content negotiation and caching
-directives to authentication tokens and file metadata. While the native
-`Headers` API provides a basic string-based interface, it leaves the
-complexities of parsing specific header formats entirely up to you.
-
-## Installation
-
-```sh
-bun add @remix-run/headers
-```
-
-## Header utilities
-
-- Accept headers: [accept-headers](./accept-headers.md)
-- Content and cache headers: [content-headers](./content-headers.md)
-- Cookies: [cookie-headers](./cookie-headers.md)
-- Conditionals and ranges: [conditional-headers](./conditional-headers.md)
-- Raw header parsing: [raw-headers](./raw-headers.md)
-
-## Navigation
-
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/headers/raw-headers.md b/docs/agents/remix/headers/raw-headers.md
deleted file mode 100644
index eeea369..0000000
--- a/docs/agents/remix/headers/raw-headers.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Raw header parsing
-
-Source: https://github.com/remix-run/remix/tree/main/packages/headers
-
-## Raw headers
-
-Parse and stringify raw HTTP header strings.
-
-```ts
-import { parse, stringify } from '@remix-run/headers'
-
-let headers = parse('Content-Type: text/html\\r\\nCache-Control: no-cache')
-headers.get('content-type') // 'text/html'
-headers.get('cache-control') // 'no-cache'
-
-stringify(headers)
-// 'Content-Type: text/html\\r\\nCache-Control: no-cache'
-```
-
-## Related packages
-
-- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) -
- Build HTTP proxy servers using the web fetch API
-- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) -
- Build HTTP servers on Node.js using the web fetch API
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Headers overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/html-template.md b/docs/agents/remix/html-template/README.md
similarity index 69%
rename from docs/agents/remix/html-template.md
rename to docs/agents/remix/html-template/README.md
index b9cd9f7..d4aef15 100644
--- a/docs/agents/remix/html-template.md
+++ b/docs/agents/remix/html-template/README.md
@@ -1,19 +1,8 @@
# html-template
-Source: https://github.com/remix-run/remix/tree/main/packages/html-template
-
-## README
-
-Safe HTML template tag with auto-escaping for JavaScript.
-
-Building HTML strings with user input is dangerous. Without proper escaping, you
-risk XSS (cross-site scripting) vulnerabilities where malicious code can be
-injected into your pages. Manual escaping is error-prone and easy to forget,
-especially when composing HTML from multiple sources.
-
-`html-template` provides a tagged template literal for safely constructing HTML
-strings with automatic escaping of interpolated values to prevent XSS
-vulnerabilities.
+Safe HTML template literals for Remix. `html-template` automatically escapes
+interpolated values to prevent XSS while still supporting explicit trusted HTML
+insertion.
## Features
@@ -28,16 +17,14 @@ vulnerabilities.
## Installation
-Install from [npm](https://www.npmjs.com/):
-
```sh
-bun add @remix-run/html-template
+npm i remix
```
## Usage
```ts
-import { html } from '@remix-run/html-template'
+import { html } from 'remix/html-template'
let userInput = ''
let greeting = html`Hello ${userInput}!
`
@@ -52,7 +39,7 @@ attacks.
If you have trusted HTML that should not be escaped, use `html.raw`:
```ts
-import { html } from '@remix-run/html-template'
+import { html } from 'remix/html-template'
let trustedIcon = ''
let button = html.raw``
@@ -69,7 +56,7 @@ input.
SafeHtml values can be nested without double-escaping:
```ts
-import { html } from '@remix-run/html-template'
+import { html } from 'remix/html-template'
let title = html`My Title
`
let content = html`Some content with ${userInput}
`
@@ -89,7 +76,7 @@ let page = html`
You can interpolate arrays of values, which will be flattened and joined:
```ts
-import { html } from '@remix-run/html-template'
+import { html } from 'remix/html-template'
let items = ['Apple', 'Banana', 'Cherry']
let list = html`
@@ -104,7 +91,7 @@ let list = html`
Use `null` or `undefined` to render nothing:
```ts
-import { html } from '@remix-run/html-template'
+import { html } from 'remix/html-template'
let showError = false
let errorMessage = 'Something went wrong'
@@ -121,7 +108,3 @@ let page = html`
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/index.md b/docs/agents/remix/index.md
index 4e2588d..c83cdd6 100644
--- a/docs/agents/remix/index.md
+++ b/docs/agents/remix/index.md
@@ -1,183 +1,50 @@
-# Remix packages
-
-Docs for every package in https://github.com/remix-run/remix/tree/main/packages.
-
-## Table of contents
-
-- [Start here](#start-here)
-- [UI and components](#ui-and-components)
-- [Routing and requests](#routing-and-requests)
-- [Data and SQL](#data-and-sql)
-- [Sessions and cookies](#sessions-and-cookies)
-- [Responses and headers](#responses-and-headers)
-- [Uploads and parsing](#uploads-and-parsing)
-- [Files and storage](#files-and-storage)
-- [Middleware and utilities](#middleware-and-utilities)
-- [Package map](#package-map)
-- [Update instructions](#update-instructions)
-
-## Start here
-
-- Building UI with Remix Component: [component](./component/index.md)
-- Routing and request handling: [fetch-router](./fetch-router/index.md) +
- [route-pattern](./route-pattern.md)
-- Sessions and cookies: [session](./session/index.md) +
- [session-middleware](./session-middleware.md) + [cookie](./cookie.md)
-- Responses, headers, and HTML safety: [response](./response/index.md) +
- [headers](./headers/index.md) + [html-template](./html-template.md)
-- Data validation and SQL tables: [data-schema](./data-schema.md) +
- [data-table](./data-table.md)
-- File upload pipelines: [form-data-middleware](./form-data-middleware.md) +
- [form-data-parser](./form-data-parser.md) +
- [multipart-parser](./multipart-parser/index.md)
-- File storage and streaming: [file-storage](./file-storage.md) +
- [file-storage-s3](./file-storage-s3.md) + [lazy-file](./lazy-file.md) +
- [fs](./fs.md)
-- Static assets and compression: [static-middleware](./static-middleware.md) +
- [compression-middleware](./compression-middleware/index.md)
-
-## epicflare adoption snapshot
-
-- Primary runtime packages in active use:
- - `remix/component`
- - `remix/fetch-router`
- - `remix/data-schema`
- - `remix/data-table`
-- D1 integration uses `remix/data-table` with a repository adapter
- (`worker/d1-data-table-adapter.ts`) instead of `remix/data-table-sqlite`.
-- Package coverage audit against installed `remix@3.0.0-alpha.3` top-level
- exports: no missing Remix package docs in this index.
-
-## UI and components
-
-- [component](./component/index.md)
- - [Getting started](./component/getting-started.md)
- - [Components](./component/components.md)
- - [Styling basics](./component/styling-basics.md)
- - [Animate basics](./component/animate-basics.md)
- - [Testing](./component/testing.md)
-- [interaction](./interaction/index.md)
- - [Event listeners and interactions](./interaction/listeners.md)
- - [Containers and disposal](./interaction/containers-and-disposal.md)
- - [Custom interactions and typed targets](./interaction/custom-interactions.md)
-
-## Routing and requests
-
-- [fetch-router](./fetch-router/index.md)
- - [Basic usage and route maps](./fetch-router/usage.md)
- - [Routing based on request method](./fetch-router/routing-methods.md)
- - [Resource-based routes](./fetch-router/routing-resources.md)
- - [Middleware and request context](./fetch-router/middleware.md)
-- [route-pattern](./route-pattern.md)
-- [node-fetch-server](./node-fetch-server/index.md)
- - [Quick start](./node-fetch-server/quick-start.md)
- - [Advanced usage](./node-fetch-server/advanced-usage.md)
- - [Migration from Express](./node-fetch-server/migration.md)
- - [Demos and benchmark](./node-fetch-server/demos-and-benchmark.md)
-- [fetch-proxy](./fetch-proxy.md)
-
-## Data and SQL
-
-- [data-schema](./data-schema.md)
-- [data-table](./data-table.md)
-- [data-table-postgres](./data-table-postgres.md)
-- [data-table-mysql](./data-table-mysql.md)
-- [data-table-sqlite](./data-table-sqlite.md)
-
-## Sessions and cookies
-
-- [session](./session/index.md)
- - [Flash data and security](./session/flash-and-security.md)
- - [Storage strategies](./session/storage-strategies.md)
- - [Related packages](./session/related.md)
-- [session-middleware](./session-middleware.md)
-- [session-storage-memcache](./session-storage-memcache.md)
-- [session-storage-redis](./session-storage-redis.md)
-- [cookie](./cookie.md)
-
-## Responses and headers
-
-- [response](./response/index.md)
- - [File responses](./response/file-responses.md)
- - [HTML responses](./response/html-responses.md)
- - [Redirect responses](./response/redirect-responses.md)
- - [Compressed responses](./response/compress-responses.md)
- - [Related packages](./response/related.md)
-- [headers](./headers/index.md)
- - [Accept headers](./headers/accept-headers.md)
- - [Content and cache headers](./headers/content-headers.md)
- - [Cookie headers](./headers/cookie-headers.md)
- - [Conditionals and ranges](./headers/conditional-headers.md)
- - [Raw header parsing](./headers/raw-headers.md)
-- [html-template](./html-template.md)
-
-## Uploads and parsing
-
-- [form-data-middleware](./form-data-middleware.md)
-- [form-data-parser](./form-data-parser.md)
-- [multipart-parser](./multipart-parser/index.md)
- - [Limits and Node bindings](./multipart-parser/limits-and-node.md)
- - [Low-level APIs](./multipart-parser/low-level.md)
- - [Benchmarks and related packages](./multipart-parser/benchmarks.md)
-
-## Files and storage
-
-- [file-storage](./file-storage.md)
-- [file-storage-s3](./file-storage-s3.md)
-- [lazy-file](./lazy-file.md)
-- [fs](./fs.md)
-- [tar-parser](./tar-parser.md)
-
-## Middleware and utilities
-
-- [compression-middleware](./compression-middleware/index.md)
- - [Options and configuration](./compression-middleware/options.md)
-- [static-middleware](./static-middleware.md)
-- [logger-middleware](./logger-middleware.md)
-- [method-override-middleware](./method-override-middleware.md)
-- [async-context-middleware](./async-context-middleware.md)
-- [mime](./mime.md)
-- [remix](./remix.md)
-
-## Package map
-
-| Package | Focus | Docs |
-| -------------------------- | ------------------------------------------ | ------------------------------------------------------------- |
-| async-context-middleware | AsyncLocalStorage context for fetch-router | [async-context-middleware](./async-context-middleware.md) |
-| component | Remix Component UI system | [component](./component/index.md) |
-| compression-middleware | Response compression for fetch-router | [compression-middleware](./compression-middleware/index.md) |
-| cookie | Cookie parsing, signing, and serialization | [cookie](./cookie.md) |
-| data-schema | Runtime validation and schema parsing | [data-schema](./data-schema.md) |
-| data-table | Typed SQL query toolkit | [data-table](./data-table.md) |
-| data-table-mysql | MySQL adapter for data-table | [data-table-mysql](./data-table-mysql.md) |
-| data-table-postgres | Postgres adapter for data-table | [data-table-postgres](./data-table-postgres.md) |
-| data-table-sqlite | SQLite adapter for data-table | [data-table-sqlite](./data-table-sqlite.md) |
-| fetch-proxy | Fetch-based HTTP proxy | [fetch-proxy](./fetch-proxy.md) |
-| fetch-router | Fetch-based router and middleware | [fetch-router](./fetch-router/index.md) |
-| file-storage | Storage abstraction for files | [file-storage](./file-storage.md) |
-| file-storage-s3 | S3 backend for file-storage | [file-storage-s3](./file-storage-s3.md) |
-| form-data-middleware | Request FormData middleware | [form-data-middleware](./form-data-middleware.md) |
-| form-data-parser | Streaming multipart/form-data parser | [form-data-parser](./form-data-parser.md) |
-| fs | Lazy file system utilities | [fs](./fs.md) |
-| headers | Header parsing and helpers | [headers](./headers/index.md) |
-| html-template | Safe HTML template tag | [html-template](./html-template.md) |
-| interaction | Event helpers and interactions | [interaction](./interaction/index.md) |
-| lazy-file | Streaming File/Blob implementation | [lazy-file](./lazy-file.md) |
-| logger-middleware | Request/response logging | [logger-middleware](./logger-middleware.md) |
-| method-override-middleware | HTML form method override | [method-override-middleware](./method-override-middleware.md) |
-| mime | MIME type utilities | [mime](./mime.md) |
-| multipart-parser | Streaming multipart parser | [multipart-parser](./multipart-parser/index.md) |
-| node-fetch-server | Fetch-based Node server | [node-fetch-server](./node-fetch-server/index.md) |
-| remix | Remix framework package | [remix](./remix.md) |
-| response | Response helpers | [response](./response/index.md) |
-| route-pattern | URL matching and href generation | [route-pattern](./route-pattern.md) |
-| session | Session management and storage | [session](./session/index.md) |
-| session-middleware | Session middleware for fetch-router | [session-middleware](./session-middleware.md) |
-| session-storage-memcache | Memcache storage adapter for sessions | [session-storage-memcache](./session-storage-memcache.md) |
-| session-storage-redis | Redis storage adapter for sessions | [session-storage-redis](./session-storage-redis.md) |
-| static-middleware | Static file middleware | [static-middleware](./static-middleware.md) |
-| tar-parser | Streaming tar parser | [tar-parser](./tar-parser.md) |
-
-## Update instructions
-
-See [update](./update.md) for how to sync this documentation from upstream.
+# Remix package docs
+
+Synced from `/tmp/remix-alpha4/packages` for `remix@3.0.0-alpha.4`.
+
+## Packages
+
+- [async-context-middleware](./async-context-middleware/README.md)
+- [auth](./auth/README.md)
+- [auth-middleware](./auth-middleware/README.md)
+- [component](./component/README.md) - 15 upstream docs files
+- [compression-middleware](./compression-middleware/README.md)
+- [cookie](./cookie/README.md)
+- [cop-middleware](./cop-middleware/README.md)
+- [cors-middleware](./cors-middleware/README.md)
+- [csrf-middleware](./csrf-middleware/README.md)
+- [data-schema](./data-schema/README.md)
+- [data-table](./data-table/README.md)
+- [data-table-mysql](./data-table-mysql/README.md)
+- [data-table-postgres](./data-table-postgres/README.md)
+- [data-table-sqlite](./data-table-sqlite/README.md)
+- [fetch-proxy](./fetch-proxy/README.md)
+- [fetch-router](./fetch-router/README.md)
+- [file-storage](./file-storage/README.md)
+- [file-storage-s3](./file-storage-s3/README.md)
+- [form-data-middleware](./form-data-middleware/README.md)
+- [form-data-parser](./form-data-parser/README.md)
+- [fs](./fs/README.md)
+- [headers](./headers/README.md)
+- [html-template](./html-template/README.md)
+- [lazy-file](./lazy-file/README.md)
+- [logger-middleware](./logger-middleware/README.md)
+- [method-override-middleware](./method-override-middleware/README.md)
+- [mime](./mime/README.md)
+- [multipart-parser](./multipart-parser/README.md)
+- [node-fetch-server](./node-fetch-server/README.md)
+- [remix](./remix/README.md)
+- [response](./response/README.md)
+- [route-pattern](./route-pattern/README.md)
+- [session](./session/README.md)
+- [session-middleware](./session-middleware/README.md)
+- [session-storage-memcache](./session-storage-memcache/README.md)
+- [session-storage-redis](./session-storage-redis/README.md)
+- [static-middleware](./static-middleware/README.md)
+- [tar-parser](./tar-parser/README.md)
+
+## Notes
+
+- Each package lives in its own directory with the upstream `README.md`.
+- Packages that ship extra markdown keep those files under `./
/docs/`.
+- See `update.md` for the local sync procedure.
diff --git a/docs/agents/remix/interaction/containers-and-disposal.md b/docs/agents/remix/interaction/containers-and-disposal.md
deleted file mode 100644
index 10c1116..0000000
--- a/docs/agents/remix/interaction/containers-and-disposal.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# Containers and disposal
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Updating listeners efficiently
-
-Use `createContainer` when you need to update listeners in place (e.g., in a
-component system). The container diffs and updates existing bindings without
-unnecessary `removeEventListener`/`addEventListener` churn.
-
-```ts
-import { createContainer } from '@remix-run/interaction'
-
-let container = createContainer(form)
-
-let formData = new FormData()
-
-container.set({
- change(event) {
- formData = new FormData(event.currentTarget)
- },
- async submit(event, signal) {
- event.preventDefault()
- await fetch('/save', { method: 'POST', body: formData, signal })
- },
-})
-
-// later - only the minimal necessary changes are rebound
-container.set({
- change(event) {
- console.log('different listener')
- },
- submit(event, signal) {
- console.log('different listener')
- },
-})
-```
-
-## Disposing listeners
-
-`on` returns a dispose function. Containers expose `dispose()`. You can also
-pass an external `AbortSignal`.
-
-```ts
-import { on, createContainer } from '@remix-run/interaction'
-
-// Using the function returned from on()
-let dispose = on(button, { click: () => {} })
-dispose()
-
-// Containers
-let container = createContainer(window)
-container.set({ resize: () => {} })
-container.dispose()
-
-// Use a signal
-let eventsController = new AbortController()
-let container = createContainer(window, {
- signal: eventsController.signal,
-})
-container.set({ resize: () => {} })
-eventsController.abort()
-```
-
-## Stop propagation semantics
-
-All DOM semantics are preserved.
-
-```ts
-on(button, {
- click: [
- (event) => {
- event.stopImmediatePropagation()
- },
- () => {
- // not called
- },
- ],
-})
-```
-
-## Navigation
-
-- [Event listeners and interactions](./listeners.md)
-- [interaction overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/interaction/custom-interactions.md b/docs/agents/remix/interaction/custom-interactions.md
deleted file mode 100644
index c47f854..0000000
--- a/docs/agents/remix/interaction/custom-interactions.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# Custom interactions and typed targets
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Custom interactions
-
-Define semantic interactions that can dispatch custom events and be reused
-declaratively.
-
-```ts
-import { defineInteraction, on, type Interaction } from '@remix-run/interaction'
-
-// Provide type safety for consumers
-declare global {
- interface HTMLElementEventMap {
- [keydownEnter]: KeyboardEvent
- }
-}
-
-function KeydownEnter(handle: Interaction) {
- if (!(handle.target instanceof HTMLElement)) return
-
- handle.on(handle.target, {
- keydown(event) {
- if (event.key === 'Enter') {
- handle.target.dispatchEvent(
- new KeyboardEvent(keydownEnter, { key: 'Enter' }),
- )
- }
- },
- })
-}
-
-// define the interaction type and setup function
-const keydownEnter = defineInteraction('keydown:enter', KeydownEnter)
-
-// usage
-let button = document.createElement('button')
-on(button, {
- [keydownEnter](event) {
- console.log('Enter key pressed')
- },
-})
-```
-
-Notes:
-
-- An interaction is initialized at most once per target, even if multiple
- listeners bind the same interaction type.
-
-## Typed event targets
-
-Use `TypedEventTarget` to get type-safe `addEventListener` and
-integrate with this library's `on` helpers.
-
-```ts
-import { TypedEventTarget, on } from '@remix-run/interaction'
-
-interface DrummerEventMap {
- kick: DrummerEvent
- snare: DrummerEvent
- hat: DrummerEvent
-}
-
-class DrummerEvent extends Event {
- constructor(type: keyof DrummerEventMap) {
- super(type)
- }
-}
-
-class Drummer extends TypedEventTarget {
- kick() {
- // ...
- this.dispatchEvent(new DrummerEvent('kick'))
- }
-}
-
-let drummer = new Drummer()
-
-// native API is NOT typed
-drummer.addEventListener('kick', (event) => {
- // event is DrummerEvent
-})
-
-// type safe with on()
-on(drummer, {
- kick: (event) => {
- // event is Dispatched
- },
-})
-```
-
-## Demos
-
-The
-[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/interaction/demos)
-contains working demos:
-
-- [`demos/async`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/async) -
- Async listeners with abort signal
-- [`demos/basic`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/basic) -
- Basic event handling
-- [`demos/form`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/form) -
- Form event handling
-- [`demos/keys`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/keys) -
- Keyboard interactions
-- [`demos/popover`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/popover) -
- Popover interactions
-- [`demos/press`](https://github.com/remix-run/remix/tree/main/packages/interaction/demos/press) -
- Press and long press interactions
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [interaction overview](./index.md)
-- [Event listeners and interactions](./listeners.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/interaction/index.md b/docs/agents/remix/interaction/index.md
deleted file mode 100644
index 1d5d08b..0000000
--- a/docs/agents/remix/interaction/index.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# interaction
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Overview
-
-Enhanced events and custom interactions for any
-[EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget).
-
-## Features
-
-- **Declarative Bindings** - Event bindings with plain objects
-- **Semantic Interactions** - Reusable "interactions" like `longPress` and
- `arrowDown`
-- **Async Support** - Listeners with reentry protection via
- [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
-- **Type Safety** - Type-safe listeners and custom `EventTarget` subclasses with
- `TypedEventTarget`
-
-## Installation
-
-```sh
-bun add @remix-run/interaction
-```
-
-## Quick start
-
-```ts
-import { on } from '@remix-run/interaction'
-
-let inputElement = document.createElement('input')
-
-on(inputElement, {
- input: (event, signal) => {
- console.log('current value', event.currentTarget.value)
- },
-})
-```
-
-## Navigation
-
-- [Event listeners and interactions](./listeners.md)
-- [Containers and disposal](./containers-and-disposal.md)
-- [Custom interactions and typed targets](./custom-interactions.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/interaction/listeners.md b/docs/agents/remix/interaction/listeners.md
deleted file mode 100644
index 970cdfb..0000000
--- a/docs/agents/remix/interaction/listeners.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Event listeners and interactions
-
-Source: https://github.com/remix-run/remix/tree/main/packages/interaction
-
-## Adding event listeners
-
-Use `on(target, listeners)` to add one or more listeners. Each listener receives
-`(event, signal)` where `signal` is aborted on reentry.
-
-```ts
-import { on } from '@remix-run/interaction'
-
-let inputElement = document.createElement('input')
-
-on(inputElement, {
- input: (event, signal) => {
- console.log('current value', event.currentTarget.value)
- },
-})
-```
-
-Listeners can be arrays. They run in order and preserve normal DOM semantics
-(including `stopImmediatePropagation`).
-
-```ts
-import { on } from '@remix-run/interaction'
-
-on(inputElement, {
- input: [
- (event) => {
- console.log('first')
- },
- {
- capture: true,
- listener(event) {
- // capture phase
- },
- },
- {
- once: true,
- listener(event) {
- console.log('only once')
- },
- },
- ],
-})
-```
-
-## Built-in interactions
-
-Builtin interactions are higher-level, semantic event types (e.g., `press`,
-`longPress`, arrow keys) exported as string constants. Consume them just like
-native events by using computed keys in your listener map. When you bind one,
-the necessary underlying host events are set up automatically.
-
-```tsx
-import { on } from '@remix-run/interaction'
-import { press, longPress } from '@remix-run/interaction/press'
-
-on(listItem, {
- [press](event) {
- navigateTo(listItem.href)
- },
-
- [longPress](event) {
- event.preventDefault() // prevents `press`
- showActions()
- },
-})
-```
-
-Import builtins from their modules (for example, `@remix-run/interaction/press`,
-`@remix-run/interaction/keys`). Some interactions may coordinate with others
-(for example, calling `event.preventDefault()` in one listener can prevent a
-related interaction from firing).
-
-## Async listeners and reentry protection
-
-The `signal` is aborted when the same listener is re-entered (for example, a
-user types quickly and triggers `input` repeatedly). Pass it to async APIs or
-check it manually to avoid stale work.
-
-```ts
-on(inputElement, {
- async input(event, signal) {
- showSearchSpinner()
-
- // Abortable fetch
- let res = await fetch(`/search?q=${event.currentTarget.value}`, { signal })
- let results = await res.json()
- updateResults(results)
- },
-})
-```
-
-For APIs that don't accept a signal:
-
-```ts
-on(inputElement, {
- async input(event, signal) {
- showSearchSpinner()
- let results = await someSearch(event.currentTarget.value)
- if (signal.aborted) return
- updateResults(results)
- },
-})
-```
-
-## Event listener options
-
-All DOM
-[`AddEventListenerOptions`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options)
-are supported via descriptors:
-
-```ts
-import { on } from '@remix-run/interaction'
-
-on(button, {
- click: {
- capture: true,
- listener(event) {
- console.log('capture phase')
- },
- },
- focus: {
- once: true,
- listener(event) {
- console.log('focused once')
- },
- },
-})
-```
-
-## Navigation
-
-- [Containers and disposal](./containers-and-disposal.md)
-- [interaction overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/lazy-file.md b/docs/agents/remix/lazy-file/README.md
similarity index 90%
rename from docs/agents/remix/lazy-file.md
rename to docs/agents/remix/lazy-file/README.md
index d4162c3..f9beb5d 100644
--- a/docs/agents/remix/lazy-file.md
+++ b/docs/agents/remix/lazy-file/README.md
@@ -1,10 +1,6 @@
# lazy-file
-Source: https://github.com/remix-run/remix/tree/main/packages/lazy-file
-
-## README
-
-`lazy-file` is a lazy, streaming `Blob`/`File` implementation for JavaScript.
+A lazy, streaming `Blob`/`File` implementation for JavaScript.
It allows you to easily create
[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob)-like and
@@ -29,7 +25,7 @@ they are streamed to avoid buffering.
[`Blob.slice()`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice),
even on streaming content
-## The Problem
+## Why You Need This
JavaScript's [File API](https://developer.mozilla.org/en-US/docs/Web/API/File)
is useful, but it's not a great fit for streaming server environments where you
@@ -56,10 +52,8 @@ All other `File` functionality works as you'd expect.
## Installation
-Install from [npm](https://www.npmjs.com/):
-
```sh
-bun add @remix-run/lazy-file
+npm i remix
```
## Usage
@@ -68,7 +62,7 @@ The low-level API can be used to create a `LazyFile` that streams content from
anywhere:
```ts
-import { type LazyContent, LazyFile } from '@remix-run/lazy-file'
+import { type LazyContent, LazyFile } from 'remix/lazy-file'
let content: LazyContent = {
// The total length of this file in bytes.
@@ -101,7 +95,7 @@ Use `.stream()` to get a `ReadableStream` for `Response` and other streaming
APIs:
```ts
-import { openLazyFile } from '@remix-run/fs'
+import { openLazyFile } from 'remix/fs'
let lazyFile = openLazyFile('./large-video.mp4')
@@ -140,7 +134,3 @@ formData.append('document', realFile)
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/logger-middleware.md b/docs/agents/remix/logger-middleware/README.md
similarity index 78%
rename from docs/agents/remix/logger-middleware.md
rename to docs/agents/remix/logger-middleware/README.md
index 0455555..8943724 100644
--- a/docs/agents/remix/logger-middleware.md
+++ b/docs/agents/remix/logger-middleware/README.md
@@ -1,25 +1,26 @@
# logger-middleware
-Source: https://github.com/remix-run/remix/tree/main/packages/logger-middleware
+HTTP request/response logging middleware for Remix. It logs request metadata and
+response details with configurable output formats.
-## README
+## Features
-Middleware for logging HTTP requests and responses for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-Logs information about HTTP requests and responses with customizable formatting.
+- **Request/Response Logging** - Logs method, path, status, and response
+ metadata
+- **Token-Based Formatting** - Customize log output with built-in placeholders
+- **Structured Timing Data** - Includes request duration and timestamps
## Installation
```sh
-bun add @remix-run/logger-middleware
+npm i remix
```
## Usage
```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { logger } from '@remix-run/logger-middleware'
+import { createRouter } from 'remix/fetch-router'
+import { logger } from 'remix/logger-middleware'
let router = createRouter({
middleware: [logger()],
@@ -33,7 +34,7 @@ let router = createRouter({
You can use the `format` option to customize the log format. The following
tokens are available:
-- `%date` - Date and time in Apache/nginx format (dd/Mon/yyyy:HH:mm:ss +/-zzzz)
+- `%date` - Date and time in Apache/nginx format (dd/Mon/yyyy:HH:mm:ss ±zzzz)
- `%dateISO` - Date and time in ISO format
- `%duration` - Request duration in milliseconds
- `%contentLength` - Response Content-Length header
@@ -104,7 +105,3 @@ let router = createRouter({
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/method-override-middleware.md b/docs/agents/remix/method-override-middleware/README.md
similarity index 70%
rename from docs/agents/remix/method-override-middleware.md
rename to docs/agents/remix/method-override-middleware/README.md
index 86fed5f..56bd1e8 100644
--- a/docs/agents/remix/method-override-middleware.md
+++ b/docs/agents/remix/method-override-middleware/README.md
@@ -1,20 +1,19 @@
# method-override-middleware
-Source:
-https://github.com/remix-run/remix/tree/main/packages/method-override-middleware
+Method override middleware for Remix. It allows HTML forms to simulate `PUT`,
+`PATCH`, and `DELETE` requests using a hidden form field.
-## README
+## Features
-Middleware for overriding HTTP request methods from form data for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-Allows HTML forms (which only support GET and POST) to submit with other HTTP
-methods like PUT, PATCH, or DELETE by including a special form field.
+- **Form Method Overrides** - Translate posted form fields into request methods
+- **HTML Form Friendly** - Supports REST-style routes from standard browser
+ forms
+- **Configurable Field Name** - Choose a custom override field key
## Installation
```sh
-bun add @remix-run/method-override-middleware
+npm i remix
```
## Usage
@@ -26,9 +25,9 @@ override field. This is useful for simulating RESTful API request methods like
PUT and DELETE using HTML forms.
```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { formData } from '@remix-run/form-data-middleware'
-import { methodOverride } from '@remix-run/method-override-middleware'
+import { createRouter } from 'remix/fetch-router'
+import { formData } from 'remix/form-data-middleware'
+import { methodOverride } from 'remix/method-override-middleware'
let router = createRouter({
// methodOverride must come AFTER formData middleware
@@ -79,7 +78,3 @@ let router = createRouter({
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/mime.md b/docs/agents/remix/mime/README.md
similarity index 75%
rename from docs/agents/remix/mime.md
rename to docs/agents/remix/mime/README.md
index 81ce6fd..151a1e4 100644
--- a/docs/agents/remix/mime.md
+++ b/docs/agents/remix/mime/README.md
@@ -1,18 +1,20 @@
-# @remix-run/mime
+# mime
-Source: https://github.com/remix-run/remix/tree/main/packages/mime
+MIME type detection and content-type helpers for Remix. This package maps
+extensions to MIME types and provides utilities for charset and compressibility
+checks.
-## README
+## Features
-Utilities for working with MIME types.
-
-Data used for these utilities is generated at build time from
-[mime-db](https://github.com/jshttp/mime-db).
+- **MIME Detection** - Detect MIME types from extensions and filenames
+- **Content-Type Helpers** - Build `Content-Type` values with charset handling
+- **Compression Signals** - Check whether a media type is likely compressible
+- **Generated Data** - Built from [mime-db](https://github.com/jshttp/mime-db)
## Installation
-```bash
-bun add @remix-run/mime
+```sh
+npm i remix
```
## Usage
@@ -22,7 +24,7 @@ bun add @remix-run/mime
Detects the MIME type for a given file extension or filename.
```ts
-import { detectMimeType } from '@remix-run/mime'
+import { detectMimeType } from 'remix/mime'
detectMimeType('txt') // 'text/plain'
detectMimeType('.txt') // 'text/plain'
@@ -38,7 +40,7 @@ including `charset` for text-based types. See
[`mimeTypeToContentType`](#mimetypetocontenttypemimetype) for charset logic.
```ts
-import { detectContentType } from '@remix-run/mime'
+import { detectContentType } from 'remix/mime'
detectContentType('css') // 'text/css; charset=utf-8'
detectContentType('.json') // 'application/json; charset=utf-8'
@@ -51,7 +53,7 @@ detectContentType('path/to/file.unknown') // undefined
Checks if a MIME type is known to be compressible.
```ts
-import { isCompressibleMimeType } from '@remix-run/mime'
+import { isCompressibleMimeType } from 'remix/mime'
isCompressibleMimeType('text/html') // true
isCompressibleMimeType('application/json') // true
@@ -62,7 +64,7 @@ isCompressibleMimeType('video/mp4') // false
For convenience, the function also accepts a full Content-Type header value:
```ts
-import { isCompressibleMimeType } from '@remix-run/mime'
+import { isCompressibleMimeType } from 'remix/mime'
isCompressibleMimeType('text/html; charset=utf-8') // true
isCompressibleMimeType('application/json; charset=utf-8') // true
@@ -78,7 +80,7 @@ declarations), `application/json`, `application/javascript`, and all `+json`
suffixed types. All other types are returned unchanged.
```ts
-import { mimeTypeToContentType } from '@remix-run/mime'
+import { mimeTypeToContentType } from 'remix/mime'
mimeTypeToContentType('text/css') // 'text/css; charset=utf-8'
mimeTypeToContentType('application/json') // 'application/json; charset=utf-8'
@@ -91,7 +93,7 @@ mimeTypeToContentType('image/png') // 'image/png'
Registers or overrides a MIME type for one or more file extensions.
```ts
-import { defineMimeType } from '@remix-run/mime'
+import { defineMimeType } from 'remix/mime'
defineMimeType({
extensions: ['myformat'],
@@ -114,7 +116,3 @@ defineMimeType({
## License
MIT
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/multipart-parser/README.md b/docs/agents/remix/multipart-parser/README.md
new file mode 100644
index 0000000..682a18a
--- /dev/null
+++ b/docs/agents/remix/multipart-parser/README.md
@@ -0,0 +1,279 @@
+# multipart-parser
+
+Fast streaming multipart parsing for JavaScript. `multipart-parser` processes
+multipart bodies incrementally so large uploads can be handled without buffering
+the entire multipart payload in memory.
+
+## Features
+
+- **File Upload Parsing** - Parse file uploads (`multipart/form-data`) with
+ automatic field and file detection
+- **Full Multipart Support** - Support for all `multipart/*` content types
+ (mixed, alternative, related, etc.)
+- **Convenient API** - `MultipartPart` API with `arrayBuffer`, `bytes`, `text`,
+ `size`, and metadata access
+- **Built-in Limits** - Header, per-part, part-count, and aggregate-size limits
+ to prevent abuse
+- **Node.js Support** - First-class Node.js support with native
+ `http.IncomingMessage` compatibility
+- **Runtime Demos** -
+ [Demos for every major runtime](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Usage
+
+The most common use case for `multipart-parser` is handling file uploads when
+you're building a web server. For this case, the `parseMultipartRequest`
+function is your friend. It automatically validates the request is
+`multipart/form-data`, extracts the multipart boundary from the `Content-Type`
+header, parses all fields and files in the `request.body` stream, and gives each
+one to you as a `MultipartPart` object with a rich API for accessing its
+metadata and content.
+
+```ts
+import {
+ MultipartParseError,
+ parseMultipartRequest,
+} from 'remix/multipart-parser'
+
+async function handleRequest(request: Request): void {
+ try {
+ for await (let part of parseMultipartRequest(request)) {
+ if (part.isFile) {
+ // Access file data in multiple formats
+ let buffer = part.arrayBuffer // ArrayBuffer
+ console.log(
+ `File received: ${part.filename} (${buffer.byteLength} bytes)`,
+ )
+ console.log(`Content type: ${part.mediaType}`)
+ console.log(`Field name: ${part.name}`)
+
+ // Save to disk, upload to cloud storage, etc.
+ await saveFile(part.filename, part.bytes)
+ } else {
+ let text = part.text // string
+ console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)
+ }
+ }
+ } catch (error) {
+ if (error instanceof MultipartParseError) {
+ console.error('Failed to parse multipart request:', error.message)
+ } else {
+ console.error('An unexpected error occurred:', error)
+ }
+ }
+}
+```
+
+## Size Limits
+
+A common use case when handling file uploads is limiting the overall shape of
+incoming multipart bodies so malicious clients cannot force unbounded growth in
+memory. Use `maxFileSize` to limit each part, `maxParts` to limit how many parts
+are accepted, and `maxTotalSize` to limit aggregate part content across the
+entire request. `multipart-parser` applies finite defaults for each of these
+limits.
+
+```ts
+import {
+ MultipartParseError,
+ MaxFileSizeExceededError,
+ MaxPartsExceededError,
+ MaxTotalSizeExceededError,
+ parseMultipartRequest,
+} from 'remix/multipart-parser/node'
+
+const oneMb = Math.pow(2, 20)
+const limits = {
+ maxFileSize: 10 * oneMb,
+ maxParts: 100,
+ maxTotalSize: 25 * oneMb,
+}
+
+async function handleRequest(request: Request): Promise {
+ try {
+ for await (let part of parseMultipartRequest(request, limits)) {
+ // ...
+ }
+ } catch (error) {
+ if (error instanceof MaxFileSizeExceededError) {
+ return new Response('File size limit exceeded', { status: 413 })
+ } else if (error instanceof MaxPartsExceededError) {
+ return new Response('Too many multipart parts', { status: 413 })
+ } else if (error instanceof MaxTotalSizeExceededError) {
+ return new Response('Multipart request is too large', { status: 413 })
+ } else if (error instanceof MultipartParseError) {
+ return new Response('Failed to parse multipart request', { status: 400 })
+ } else {
+ console.error(error)
+ return new Response('Internal Server Error', { status: 500 })
+ }
+ }
+}
+```
+
+## Node.js Bindings
+
+The main module (`import {} from 'remix/multipart-parser'`) assumes you're
+working with
+[the fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
+(`Request`, `ReadableStream`, etc). Support for these interfaces was added to
+Node.js by the [undici](https://github.com/nodejs/undici) project in
+[version 16.5.0](https://nodejs.org/en/blog/release/v16.5.0).
+
+If however you're building a server for Node.js that relies on node-specific
+APIs like `http.IncomingMessage`, `stream.Readable`, and `buffer.Buffer` (ala
+Express or `http.createServer`), `multipart-parser` ships with an additional
+module that works directly with these APIs.
+
+```ts
+import * as http from 'node:http'
+import {
+ MultipartParseError,
+ parseMultipartRequest,
+} from 'remix/multipart-parser/node'
+
+let server = http.createServer(async (req, res) => {
+ try {
+ for await (let part of parseMultipartRequest(req)) {
+ // ...
+ }
+ } catch (error) {
+ if (error instanceof MultipartParseError) {
+ console.error('Failed to parse multipart request:', error.message)
+ } else {
+ console.error('An unexpected error occurred:', error)
+ }
+ }
+})
+
+server.listen(8080)
+```
+
+## Low-level API
+
+If you're working directly with multipart boundaries and buffers/streams of
+multipart data that are not necessarily part of a request, `multipart-parser`
+provides a low-level `parseMultipart()` API that you can use directly:
+
+```ts
+import { parseMultipart } from 'remix/multipart-parser'
+
+let message = new Uint8Array(/* ... */)
+let boundary = '----WebKitFormBoundary56eac3x'
+
+for (let part of parseMultipart(message, { boundary })) {
+ // ...
+}
+```
+
+In addition, the `parseMultipartStream` function provides an `async` generator
+interface for multipart data in a `ReadableStream`:
+
+```ts
+import { parseMultipartStream } from 'remix/multipart-parser'
+
+let message = new ReadableStream(/* ... */)
+let boundary = '----WebKitFormBoundary56eac3x'
+
+for await (let part of parseMultipartStream(message, { boundary })) {
+ // ...
+}
+```
+
+## Demos
+
+The
+[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
+contains a few working demos of how you can use this library:
+
+- [`demos/bun`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/bun) -
+ using multipart-parser in Bun
+- [`demos/cf-workers`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/cf-workers) -
+ using multipart-parser in a Cloudflare Worker and storing file uploads in R2
+- [`demos/deno`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/deno) -
+ using multipart-parser in Deno
+- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/node) -
+ using multipart-parser in Node.js
+
+## Benchmark
+
+`multipart-parser` is designed to be as efficient as possible, operating on
+streams of data and rarely buffering in common usage. This design yields
+exceptional performance when handling multipart payloads of any size. In
+benchmarks, `multipart-parser` is as fast or faster than `busboy`.
+
+The results of running the benchmarks on my laptop:
+
+```
+> @remix-run/multipart-parser@0.10.1 bench:node /Users/michael/Projects/remix-the-web/packages/multipart-parser
+> node ./bench/runner.ts
+
+Platform: Darwin (24.5.0)
+CPU: Apple M1 Pro
+Date: 6/13/2025, 12:27:09 PM
+Node.js v24.0.2
+┌──────────────────┬──────────────────┬──────────────────┬──────────────────┬───────────────────┐
+│ (index) │ 1 small file │ 1 large file │ 100 small files │ 5 large files │
+├──────────────────┼──────────────────┼──────────────────┼──────────────────┼───────────────────┤
+│ multipart-parser │ '0.01 ms ± 0.03' │ '1.08 ms ± 0.08' │ '0.04 ms ± 0.01' │ '10.50 ms ± 0.38' │
+│ multipasta │ '0.02 ms ± 0.06' │ '1.07 ms ± 0.02' │ '0.15 ms ± 0.02' │ '10.46 ms ± 0.11' │
+│ busboy │ '0.06 ms ± 0.17' │ '3.07 ms ± 0.24' │ '0.24 ms ± 0.05' │ '29.85 ms ± 0.18' │
+│ @fastify/busboy │ '0.05 ms ± 0.13' │ '1.23 ms ± 0.09' │ '0.45 ms ± 0.22' │ '11.81 ms ± 0.11' │
+└──────────────────┴──────────────────┴──────────────────┴──────────────────┴───────────────────┘
+
+> @remix-run/multipart-parser@0.10.1 bench:bun /Users/michael/Projects/remix-the-web/packages/multipart-parser
+> bun run ./bench/runner.ts
+
+Platform: Darwin (24.5.0)
+CPU: Apple M1 Pro
+Date: 6/13/2025, 12:27:31 PM
+Bun 1.2.13
+┌──────────────────┬────────────────┬────────────────┬─────────────────┬─────────────────┐
+│ │ 1 small file │ 1 large file │ 100 small files │ 5 large files │
+├──────────────────┼────────────────┼────────────────┼─────────────────┼─────────────────┤
+│ multipart-parser │ 0.01 ms ± 0.04 │ 0.86 ms ± 0.09 │ 0.04 ms ± 0.01 │ 8.32 ms ± 0.26 │
+│ multipasta │ 0.02 ms ± 0.07 │ 0.87 ms ± 0.03 │ 0.25 ms ± 0.21 │ 8.27 ms ± 0.09 │
+│ busboy │ 0.05 ms ± 0.17 │ 3.54 ms ± 0.10 │ 0.30 ms ± 0.03 │ 34.79 ms ± 0.38 │
+│ @fastify/busboy │ 0.06 ms ± 0.18 │ 4.04 ms ± 0.08 │ 0.48 ms ± 0.06 │ 39.91 ms ± 0.37 │
+└──────────────────┴────────────────┴────────────────┴─────────────────┴─────────────────┘
+
+> @remix-run/multipart-parser@0.10.1 bench:deno /Users/michael/Projects/remix-the-web/packages/multipart-parser
+> deno run --allow-sys ./bench/runner.ts
+
+Platform: Darwin (24.5.0)
+CPU: Apple M1 Pro
+Date: 6/13/2025, 12:28:12 PM
+Deno 2.3.6
+┌──────────────────┬──────────────────┬────────────────────┬──────────────────┬─────────────────────┐
+│ (idx) │ 1 small file │ 1 large file │ 100 small files │ 5 large files │
+├──────────────────┼──────────────────┼────────────────────┼──────────────────┼─────────────────────┤
+│ multipart-parser │ "0.01 ms ± 0.03" │ "1.03 ms ± 0.04" │ "0.05 ms ± 0.01" │ "10.05 ms ± 0.20" │
+│ multipasta │ "0.02 ms ± 0.07" │ "1.04 ms ± 0.03" │ "0.16 ms ± 0.02" │ "10.10 ms ± 0.08" │
+│ busboy │ "0.05 ms ± 0.19" │ "3.06 ms ± 0.15" │ "0.32 ms ± 0.05" │ "29.92 ms ± 0.24" │
+│ @fastify/busboy │ "0.06 ms ± 0.14" │ "14.72 ms ± 11.42" │ "0.81 ms ± 0.20" │ "127.63 ms ± 35.77" │
+└──────────────────┴──────────────────┴────────────────────┴──────────────────┴─────────────────────┘
+```
+
+## Related Packages
+
+- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
+ Uses `multipart-parser` internally to parse multipart requests and generate
+ `FileUpload`s for storage
+- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) -
+ Used internally to parse HTTP headers and get metadata (filename, content
+ type) for each `MultipartPart`
+
+## Credits
+
+Thanks to Jacob Ebey who gave me several code reviews on this project prior to
+publishing.
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/multipart-parser/benchmarks.md b/docs/agents/remix/multipart-parser/benchmarks.md
deleted file mode 100644
index 2603635..0000000
--- a/docs/agents/remix/multipart-parser/benchmarks.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Benchmarks and related packages
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Demos
-
-The
-[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
-contains working demos:
-
-- [`demos/bun`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/bun) -
- using multipart-parser in Bun
-- [`demos/cf-workers`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/cf-workers) -
- using multipart-parser in a Cloudflare Worker and storing file uploads in R2
-- [`demos/deno`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/deno) -
- using multipart-parser in Deno
-- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/node) -
- using multipart-parser in Node.js
-
-## Benchmark
-
-`multipart-parser` is designed to be as efficient as possible, operating on
-streams of data and rarely buffering in common usage. This design yields
-exceptional performance when handling multipart payloads of any size. In
-benchmarks, `multipart-parser` is as fast or faster than `busboy`.
-
-The results of running the benchmarks on my laptop:
-
-```
-> @remix-run/multipart-parser@0.10.1 bench:node /Users/michael/Projects/remix-the-web/packages/multipart-parser
-> node --disable-warning=ExperimentalWarning ./bench/runner.ts
-
-Platform: Darwin (24.5.0)
-CPU: Apple M1 Pro
-Date: 6/13/2025, 12:27:09 PM
-Node.js v24.0.2
-(index) | 1 small file | 1 large file | 100 small files | 5 large files
-multipart-parser | '0.01 ms +/- 0.03' | '1.08 ms +/- 0.08' | '0.04 ms +/- 0.01' | '10.50 ms +/- 0.38'
-multipasta | '0.02 ms +/- 0.06' | '1.07 ms +/- 0.02' | '0.15 ms +/- 0.02' | '10.46 ms +/- 0.11'
-busboy | '0.06 ms +/- 0.17' | '3.07 ms +/- 0.24' | '0.24 ms +/- 0.05' | '29.85 ms +/- 0.18'
-@fastify/busboy | '0.05 ms +/- 0.13' | '1.23 ms +/- 0.09' | '0.45 ms +/- 0.22' | '11.81 ms +/- 0.11'
-
-> @remix-run/multipart-parser@0.10.1 bench:bun /Users/michael/Projects/remix-the-web/packages/multipart-parser
-> bun run ./bench/runner.ts
-
-Platform: Darwin (24.5.0)
-CPU: Apple M1 Pro
-Date: 6/13/2025, 12:27:31 PM
-Bun 1.2.13
-(index) | 1 small file | 1 large file | 100 small files | 5 large files
-multipart-parser | 0.01 ms +/- 0.04 | 0.86 ms +/- 0.09 | 0.04 ms +/- 0.01 | 8.32 ms +/- 0.26
-multipasta | 0.02 ms +/- 0.07 | 0.87 ms +/- 0.03 | 0.25 ms +/- 0.21 | 8.27 ms +/- 0.09
-busboy | 0.05 ms +/- 0.17 | 3.54 ms +/- 0.10 | 0.30 ms +/- 0.03 | 34.79 ms +/- 0.38
-@fastify/busboy | 0.06 ms +/- 0.18 | 4.04 ms +/- 0.08 | 0.48 ms +/- 0.06 | 39.91 ms +/- 0.37
-
-> @remix-run/multipart-parser@0.10.1 bench:deno /Users/michael/Projects/remix-the-web/packages/multipart-parser
-> deno run --allow-sys ./bench/runner.ts
-
-Platform: Darwin (24.5.0)
-CPU: Apple M1 Pro
-Date: 6/13/2025, 12:28:12 PM
-Deno 2.3.6
-(idx) | 1 small file | 1 large file | 100 small files | 5 large files
-multipart-parser | "0.01 ms +/- 0.03" | "1.03 ms +/- 0.04" | "0.05 ms +/- 0.01" | "10.05 ms +/- 0.20"
-multipasta | "0.02 ms +/- 0.07" | "1.04 ms +/- 0.03" | "0.16 ms +/- 0.02" | "10.10 ms +/- 0.08"
-busboy | "0.05 ms +/- 0.19" | "3.06 ms +/- 0.15" | "0.32 ms +/- 0.05" | "29.92 ms +/- 0.24"
-@fastify/busboy | "0.06 ms +/- 0.14" | "14.72 ms +/- 11.42" | "0.81 ms +/- 0.20" | "127.63 ms +/- 35.77"
-```
-
-## Related packages
-
-- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) -
- Uses `multipart-parser` internally to parse multipart requests and generate
- `FileUpload`s for storage
-- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) -
- Used internally to parse HTTP headers and get metadata (filename, content
- type) for each `MultipartPart`
-
-## Credits
-
-Thanks to Jacob Ebey who gave me several code reviews on this project prior to
-publishing.
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [multipart-parser overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/multipart-parser/index.md b/docs/agents/remix/multipart-parser/index.md
deleted file mode 100644
index b71de1a..0000000
--- a/docs/agents/remix/multipart-parser/index.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# multipart-parser
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Overview
-
-`multipart-parser` is a fast, streaming multipart parser that works in any
-JavaScript environment. Whether you're handling file uploads, parsing email
-attachments, or working with multipart API responses, `multipart-parser` has you
-covered.
-
-## Why multipart-parser?
-
-- **Universal JavaScript** - One library that works everywhere: Node.js, Bun,
- Deno, Cloudflare Workers, and browsers
-- **Blazing Fast** - Outperforms popular alternatives like busboy in benchmarks
-- **Zero Dependencies** - Lightweight and secure with no external dependencies
-- **Memory Efficient** - Streaming architecture that yields files as they are
- found in the stream
-- **Type Safe** - Written in TypeScript with comprehensive type definitions
-- **Standards Based** - Built on the web Streams API for maximum compatibility
-- **Production Ready** - Battle-tested error handling with specific error types
-
-## Installation
-
-```sh
-bun add @remix-run/multipart-parser
-```
-
-## Usage
-
-The most common use case is handling file uploads when you're building a web
-server. The `parseMultipartRequest` function validates the request, extracts the
-multipart boundary from the `Content-Type` header, parses all fields and files
-in the `request.body` stream, and gives each one to you as a `MultipartPart`
-object.
-
-```ts
-import {
- MultipartParseError,
- parseMultipartRequest,
-} from '@remix-run/multipart-parser'
-
-async function handleRequest(request: Request): void {
- try {
- for await (let part of parseMultipartRequest(request)) {
- if (part.isFile) {
- // Access file data in multiple formats
- let buffer = part.arrayBuffer // ArrayBuffer
- console.log(
- `File received: ${part.filename} (${buffer.byteLength} bytes)`,
- )
- console.log(`Content type: ${part.mediaType}`)
- console.log(`Field name: ${part.name}`)
-
- // Save to disk, upload to cloud storage, etc.
- await saveFile(part.filename, part.bytes)
- } else {
- let text = part.text // string
- console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)
- }
- }
- } catch (error) {
- if (error instanceof MultipartParseError) {
- console.error('Failed to parse multipart request:', error.message)
- } else {
- console.error('An unexpected error occurred:', error)
- }
- }
-}
-```
-
-## Navigation
-
-- [Limits and Node bindings](./limits-and-node.md)
-- [Low-level APIs](./low-level.md)
-- [Benchmarks and related packages](./benchmarks.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/multipart-parser/limits-and-node.md b/docs/agents/remix/multipart-parser/limits-and-node.md
deleted file mode 100644
index 7d2dcc8..0000000
--- a/docs/agents/remix/multipart-parser/limits-and-node.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Limits and Node bindings
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Limiting file upload size
-
-You can set a file upload size limit using the `maxFileSize` option, and return
-a 413 "Payload Too Large" response when you receive a request that exceeds the
-limit.
-
-```ts
-import {
- MultipartParseError,
- MaxFileSizeExceededError,
- parseMultipartRequest,
-} from '@remix-run/multipart-parser/node'
-
-const oneMb = Math.pow(2, 20)
-const maxFileSize = 10 * oneMb
-
-async function handleRequest(request: Request): Promise {
- try {
- for await (let part of parseMultipartRequest(request, { maxFileSize })) {
- // ...
- }
- } catch (error) {
- if (error instanceof MaxFileSizeExceededError) {
- return new Response('File size limit exceeded', { status: 413 })
- } else if (error instanceof MultipartParseError) {
- return new Response('Failed to parse multipart request', { status: 400 })
- } else {
- console.error(error)
- return new Response('Internal Server Error', { status: 500 })
- }
- }
-}
-```
-
-## Node.js bindings
-
-The main module (`import from "@remix-run/multipart-parser"`) assumes you're
-working with the Fetch API (`Request`, `ReadableStream`, etc). Support for these
-interfaces was added to Node.js by the undici project in version 16.5.0.
-
-If you're building a server for Node.js that relies on node-specific APIs like
-`http.IncomingMessage`, `stream.Readable`, and `buffer.Buffer`,
-`multipart-parser` ships with an additional module that works directly with
-these APIs.
-
-```ts
-import * as http from 'node:http'
-import {
- MultipartParseError,
- parseMultipartRequest,
-} from '@remix-run/multipart-parser/node'
-
-let server = http.createServer(async (req, res) => {
- try {
- for await (let part of parseMultipartRequest(req)) {
- // ...
- }
- } catch (error) {
- if (error instanceof MultipartParseError) {
- console.error('Failed to parse multipart request:', error.message)
- } else {
- console.error('An unexpected error occurred:', error)
- }
- }
-})
-
-server.listen(8080)
-```
-
-## Navigation
-
-- [multipart-parser overview](./index.md)
-- [Low-level APIs](./low-level.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/multipart-parser/low-level.md b/docs/agents/remix/multipart-parser/low-level.md
deleted file mode 100644
index 5df5773..0000000
--- a/docs/agents/remix/multipart-parser/low-level.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Low-level APIs
-
-Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser
-
-## Low-level API
-
-If you're working directly with multipart boundaries and buffers/streams of
-multipart data that are not necessarily part of a request, `multipart-parser`
-provides a low-level `parseMultipart()` API that you can use directly:
-
-```ts
-import { parseMultipart } from '@remix-run/multipart-parser'
-
-let message = new Uint8Array(/* ... */)
-let boundary = '----WebKitFormBoundary56eac3x'
-
-for (let part of parseMultipart(message, { boundary })) {
- // ...
-}
-```
-
-In addition, the `parseMultipartStream` function provides an async generator
-interface for multipart data in a `ReadableStream`:
-
-```ts
-import { parseMultipartStream } from '@remix-run/multipart-parser'
-
-let message = new ReadableStream(/* ... */)
-let boundary = '----WebKitFormBoundary56eac3x'
-
-for await (let part of parseMultipartStream(message, { boundary })) {
- // ...
-}
-```
-
-## Navigation
-
-- [multipart-parser overview](./index.md)
-- [Benchmarks and related packages](./benchmarks.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/README.md b/docs/agents/remix/node-fetch-server/README.md
new file mode 100644
index 0000000..0e46229
--- /dev/null
+++ b/docs/agents/remix/node-fetch-server/README.md
@@ -0,0 +1,342 @@
+# node-fetch-server
+
+Build Node.js servers with web-standard Fetch API primitives.
+`node-fetch-server` converts Node's HTTP server interfaces into
+[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)/[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
+flows that match modern runtimes.
+
+## Features
+
+- **Web Standards** - Standard
+ [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and
+ [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs
+- **Drop-in Integration** - Works with `node:http` and `node:https` modules
+- **Streaming Support** - Response support with `ReadableStream`
+- **Custom Hostname** - Configuration for deployment flexibility
+- **Client Info** - Access to client connection info (IP address, port)
+- **TypeScript** - Full TypeScript support with type definitions
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Quick Start
+
+### Basic Server
+
+Here's a complete working example with a simple in-memory data store:
+
+```ts
+import * as http from 'node:http'
+import { createRequestListener } from 'remix/node-fetch-server'
+
+// Example: Simple in-memory user storage
+let users = new Map([
+ ['1', { id: '1', name: 'Alice', email: 'alice@example.com' }],
+ ['2', { id: '2', name: 'Bob', email: 'bob@example.com' }],
+])
+
+async function handler(request: Request) {
+ let url = new URL(request.url)
+
+ // GET / - Home page
+ if (url.pathname === '/' && request.method === 'GET') {
+ return new Response('Welcome to the User API! Try GET /api/users')
+ }
+
+ // GET /api/users - List all users
+ if (url.pathname === '/api/users' && request.method === 'GET') {
+ return Response.json(Array.from(users.values()))
+ }
+
+ // GET /api/users/:id - Get specific user
+ let userMatch = url.pathname.match(/^\/api\/users\/(\w+)$/)
+ if (userMatch && request.method === 'GET') {
+ let user = users.get(userMatch[1])
+ if (user) {
+ return Response.json(user)
+ }
+ return new Response('User not found', { status: 404 })
+ }
+
+ return new Response('Not Found', { status: 404 })
+}
+
+// Create a standard Node.js server
+let server = http.createServer(createRequestListener(handler))
+
+server.listen(3000, () => {
+ console.log('Server running at http://localhost:3000')
+})
+```
+
+### Working with Request Data
+
+Handle different types of request data using standard web APIs:
+
+```ts
+async function handler(request: Request) {
+ let url = new URL(request.url)
+
+ // Handle JSON data
+ if (request.method === 'POST' && url.pathname === '/api/users') {
+ try {
+ let userData = await request.json()
+
+ // Validate required fields
+ if (!userData.name || !userData.email) {
+ return Response.json(
+ { error: 'Name and email are required' },
+ { status: 400 },
+ )
+ }
+
+ // Create user (your implementation)
+ let newUser = {
+ id: Date.now().toString(),
+ ...userData,
+ }
+
+ return Response.json(newUser, { status: 201 })
+ } catch (error) {
+ return Response.json({ error: 'Invalid JSON' }, { status: 400 })
+ }
+ }
+
+ // Handle URL search params
+ if (url.pathname === '/api/search') {
+ let query = url.searchParams.get('q')
+ let limit = parseInt(url.searchParams.get('limit') || '10')
+
+ return Response.json({
+ query,
+ limit,
+ results: [], // Your search results here
+ })
+ }
+
+ return new Response('Not Found', { status: 404 })
+}
+```
+
+### Streaming Responses
+
+Take advantage of web-standard streaming with `ReadableStream`:
+
+```ts
+async function handler(request: Request) {
+ if (request.url.endsWith('/stream')) {
+ // Create a streaming response
+ let stream = new ReadableStream({
+ async start(controller) {
+ for (let i = 0; i < 5; i++) {
+ controller.enqueue(new TextEncoder().encode(`Chunk ${i}\n`))
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ }
+ controller.close()
+ },
+ })
+
+ return new Response(stream, {
+ headers: { 'Content-Type': 'text/plain' },
+ })
+ }
+
+ return new Response('Not Found', { status: 404 })
+}
+```
+
+### Custom Hostname Configuration
+
+Configure custom hostnames for deployment on VPS or custom environments:
+
+```ts
+import * as http from 'node:http'
+import { createRequestListener } from 'remix/node-fetch-server'
+
+// Use a custom hostname (e.g., from environment variable)
+let hostname = process.env.HOST || 'api.example.com'
+
+async function handler(request: Request) {
+ // request.url will now use your custom hostname
+ console.log(request.url) // https://api.example.com/path
+
+ return Response.json({
+ message: 'Hello from custom domain!',
+ url: request.url,
+ })
+}
+
+let server = http.createServer(
+ createRequestListener(handler, { host: hostname }),
+)
+
+server.listen(3000)
+```
+
+### Accessing Client Information
+
+Get client connection details (IP address, port) for logging or security:
+
+```ts
+import { type FetchHandler } from 'remix/node-fetch-server'
+
+let handler: FetchHandler = async (request, client) => {
+ // Log client information
+ console.log(`Request from ${client.address}:${client.port}`)
+
+ // Use for rate limiting, geolocation, etc.
+ if (isRateLimited(client.address)) {
+ return new Response('Too Many Requests', { status: 429 })
+ }
+
+ return Response.json({
+ message: 'Hello!',
+ yourIp: client.address,
+ })
+}
+```
+
+### HTTPS Support
+
+Use with Node.js HTTPS module for secure connections:
+
+```ts
+import * as https from 'node:https'
+import * as fs from 'node:fs'
+import { createRequestListener } from 'remix/node-fetch-server'
+
+let options = {
+ key: fs.readFileSync('private-key.pem'),
+ cert: fs.readFileSync('certificate.pem'),
+}
+
+let server = https.createServer(options, createRequestListener(handler))
+
+server.listen(443, () => {
+ console.log('HTTPS Server running on port 443')
+})
+```
+
+## Advanced Usage
+
+### Low-level API
+
+For more control over request/response handling, use the low-level API:
+
+```ts
+import * as http from 'node:http'
+import { createRequest, sendResponse } from 'remix/node-fetch-server'
+
+let server = http.createServer(async (req, res) => {
+ // Convert Node.js request to Fetch API Request
+ let request = createRequest(req, res, { host: process.env.HOST })
+
+ try {
+ // Add custom headers or middleware logic
+ let startTime = Date.now()
+
+ // Process the request with your handler
+ let response = await handler(request)
+ // Make sure the response is mutable
+ response = new Response(response.body, response)
+
+ // Add response timing header
+ let duration = Date.now() - startTime
+ response.headers.set('X-Response-Time', `${duration}ms`)
+
+ // Send the response
+ await sendResponse(res, response)
+ } catch (error) {
+ console.error('Server error:', error)
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
+ res.end('Internal Server Error')
+ }
+})
+
+server.listen(3000)
+```
+
+The low-level API provides:
+
+- `createRequest(req, res, options)` - Converts Node.js IncomingMessage to web
+ Request
+- `sendResponse(res, response)` - Sends web Response using Node.js
+ ServerResponse
+
+This is useful for:
+
+- Building custom middleware systems
+- Integrating with existing Node.js code
+- Implementing custom error handling
+- Performance-critical applications
+
+## Migration from Express
+
+Transitioning from Express? Here's a comparison of common patterns:
+
+### Basic Routing
+
+```ts
+// Express
+let app = express()
+
+app.get('/users/:id', async (req, res) => {
+ let user = await db.getUser(req.params.id)
+ if (!user) {
+ return res.status(404).json({ error: 'User not found' })
+ }
+ res.json(user)
+})
+
+app.listen(3000)
+
+// node-fetch-server
+import { createRequestListener } from 'remix/node-fetch-server'
+
+async function handler(request: Request) {
+ let url = new URL(request.url)
+ let match = url.pathname.match(/^\/users\/(\w+)$/)
+
+ if (match && request.method === 'GET') {
+ let user = await db.getUser(match[1])
+ if (!user) {
+ return Response.json({ error: 'User not found' }, { status: 404 })
+ }
+ return Response.json(user)
+ }
+
+ return new Response('Not Found', { status: 404 })
+}
+
+http.createServer(createRequestListener(handler)).listen(3000)
+```
+
+## Demos
+
+The
+[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos)
+contains working demos:
+
+- [`demos/http2`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos/http2) -
+ HTTP/2 server with TLS certificates
+
+## Benchmark
+
+To run benchmarks comparing `node-fetch-server` performance with comparable
+libraries:
+
+```sh
+pnpm run bench
+```
+
+## Related Packages
+
+- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) -
+ Build HTTP proxy servers using the web fetch API
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/node-fetch-server/advanced-usage.md b/docs/agents/remix/node-fetch-server/advanced-usage.md
deleted file mode 100644
index b4f1007..0000000
--- a/docs/agents/remix/node-fetch-server/advanced-usage.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Advanced usage
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Low-level API
-
-For more control over request/response handling, use the low-level API:
-
-```ts
-import * as http from 'node:http'
-import { createRequest, sendResponse } from '@remix-run/node-fetch-server'
-
-let server = http.createServer(async (req, res) => {
- // Convert Node.js request to Fetch API Request
- let request = createRequest(req, res, { host: process.env.HOST })
-
- try {
- // Add custom headers or middleware logic
- let startTime = Date.now()
-
- // Process the request with your handler
- let response = await handler(request)
-
- // Add response timing header
- let duration = Date.now() - startTime
- response.headers.set('X-Response-Time', `${duration}ms`)
-
- // Send the response
- await sendResponse(res, response)
- } catch (error) {
- console.error('Server error:', error)
- res.writeHead(500, { 'Content-Type': 'text/plain' })
- res.end('Internal Server Error')
- }
-})
-
-server.listen(3000)
-```
-
-The low-level API provides:
-
-- `createRequest(req, res, options)` - Converts Node.js IncomingMessage to web
- Request
-- `sendResponse(res, response)` - Sends web Response using Node.js
- ServerResponse
-
-This is useful for:
-
-- Building custom middleware systems
-- Integrating with existing Node.js code
-- Implementing custom error handling
-- Performance-critical applications
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Migration from Express](./migration.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/demos-and-benchmark.md b/docs/agents/remix/node-fetch-server/demos-and-benchmark.md
deleted file mode 100644
index aab88a9..0000000
--- a/docs/agents/remix/node-fetch-server/demos-and-benchmark.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Demos and benchmark
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Demos
-
-The
-[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos)
-contains working demos:
-
-- [`demos/http2`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos/http2) -
- HTTP/2 server with TLS certificates
-
-## Benchmark
-
-To run benchmarks comparing `node-fetch-server` performance with comparable
-libraries:
-
-```sh
-pnpm run bench
-```
-
-## Related packages
-
-- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) -
- Build HTTP proxy servers using the web fetch API
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/index.md b/docs/agents/remix/node-fetch-server/index.md
deleted file mode 100644
index c0690ae..0000000
--- a/docs/agents/remix/node-fetch-server/index.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# node-fetch-server
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Overview
-
-Build portable Node.js servers using web-standard Fetch API primitives.
-
-`node-fetch-server` brings the simplicity and familiarity of the
-[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to
-Node.js server development. Instead of dealing with Node's traditional
-`req`/`res` objects, you work with web-standard
-[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and
-[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
-objects - the same APIs you already use in the browser and modern JavaScript
-runtimes.
-
-## Features
-
-- **Web Standards** - Standard `Request` and `Response` APIs
-- **Drop-in Integration** - Works with `node:http` and `node:https`
-- **Streaming Support** - Response support with `ReadableStream`
-- **Custom Hostname** - Configuration for deployment flexibility
-- **Client Info** - Access to client connection info (IP address, port)
-- **TypeScript** - Full TypeScript support with type definitions
-
-## Installation
-
-```sh
-bun add @remix-run/node-fetch-server
-```
-
-## Navigation
-
-- [Quick start examples](./quick-start.md)
-- [Advanced usage](./advanced-usage.md)
-- [Migration from Express](./migration.md)
-- [Demos and benchmark](./demos-and-benchmark.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/migration.md b/docs/agents/remix/node-fetch-server/migration.md
deleted file mode 100644
index 63dec91..0000000
--- a/docs/agents/remix/node-fetch-server/migration.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Migration from Express
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Basic routing
-
-```ts
-// Express
-let app = express()
-
-app.get('/users/:id', async (req, res) => {
- let user = await db.getUser(req.params.id)
- if (!user) {
- return res.status(404).json({ error: 'User not found' })
- }
- res.json(user)
-})
-
-app.listen(3000)
-
-// node-fetch-server
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-async function handler(request: Request) {
- let url = new URL(request.url)
- let match = url.pathname.match(/^\\/users\\/(\\w+)$/)
-
- if (match && request.method === 'GET') {
- let user = await db.getUser(match[1])
- if (!user) {
- return Response.json({ error: 'User not found' }, { status: 404 })
- }
- return Response.json(user)
- }
-
- return new Response('Not Found', { status: 404 })
-}
-
-http.createServer(createRequestListener(handler)).listen(3000)
-```
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Demos and benchmark](./demos-and-benchmark.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/node-fetch-server/quick-start.md b/docs/agents/remix/node-fetch-server/quick-start.md
deleted file mode 100644
index 3ed2e0b..0000000
--- a/docs/agents/remix/node-fetch-server/quick-start.md
+++ /dev/null
@@ -1,193 +0,0 @@
-# Quick start
-
-Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server
-
-## Basic server
-
-```ts
-import * as http from 'node:http'
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-// Example: Simple in-memory user storage
-let users = new Map([
- ['1', { id: '1', name: 'Alice', email: 'alice@example.com' }],
- ['2', { id: '2', name: 'Bob', email: 'bob@example.com' }],
-])
-
-async function handler(request: Request) {
- let url = new URL(request.url)
-
- // GET / - Home page
- if (url.pathname === '/' && request.method === 'GET') {
- return new Response('Welcome to the User API! Try GET /api/users')
- }
-
- // GET /api/users - List all users
- if (url.pathname === '/api/users' && request.method === 'GET') {
- return Response.json(Array.from(users.values()))
- }
-
- // GET /api/users/:id - Get specific user
- let userMatch = url.pathname.match(/^\\/api\\/users\\/(\\w+)$/)
- if (userMatch && request.method === 'GET') {
- let user = users.get(userMatch[1])
- if (user) {
- return Response.json(user)
- }
- return new Response('User not found', { status: 404 })
- }
-
- return new Response('Not Found', { status: 404 })
-}
-
-// Create a standard Node.js server
-let server = http.createServer(createRequestListener(handler))
-
-server.listen(3000, () => {
- console.log('Server running at http://localhost:3000')
-})
-```
-
-## Working with request data
-
-```ts
-async function handler(request: Request) {
- let url = new URL(request.url)
-
- // Handle JSON data
- if (request.method === 'POST' && url.pathname === '/api/users') {
- try {
- let userData = await request.json()
-
- // Validate required fields
- if (!userData.name || !userData.email) {
- return Response.json(
- { error: 'Name and email are required' },
- { status: 400 },
- )
- }
-
- // Create user (your implementation)
- let newUser = {
- id: Date.now().toString(),
- ...userData,
- }
-
- return Response.json(newUser, { status: 201 })
- } catch (error) {
- return Response.json({ error: 'Invalid JSON' }, { status: 400 })
- }
- }
-
- // Handle URL search params
- if (url.pathname === '/api/search') {
- let query = url.searchParams.get('q')
- let limit = parseInt(url.searchParams.get('limit') || '10')
-
- return Response.json({
- query,
- limit,
- results: [], // Your search results here
- })
- }
-
- return new Response('Not Found', { status: 404 })
-}
-```
-
-## Streaming responses
-
-```ts
-async function handler(request: Request) {
- if (request.url.endsWith('/stream')) {
- // Create a streaming response
- let stream = new ReadableStream({
- async start(controller) {
- for (let i = 0; i < 5; i++) {
- controller.enqueue(new TextEncoder().encode(`Chunk ${i}\\n`))
- await new Promise((resolve) => setTimeout(resolve, 1000))
- }
- controller.close()
- },
- })
-
- return new Response(stream, {
- headers: { 'Content-Type': 'text/plain' },
- })
- }
-
- return new Response('Not Found', { status: 404 })
-}
-```
-
-## Custom hostname configuration
-
-```ts
-import * as http from 'node:http'
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-// Use a custom hostname (e.g., from environment variable)
-let hostname = process.env.HOST || 'api.example.com'
-
-async function handler(request: Request) {
- // request.url will now use your custom hostname
- console.log(request.url) // https://api.example.com/path
-
- return Response.json({
- message: 'Hello from custom domain!',
- url: request.url,
- })
-}
-
-let server = http.createServer(
- createRequestListener(handler, { host: hostname }),
-)
-
-server.listen(3000)
-```
-
-## Accessing client information
-
-```ts
-import { type FetchHandler } from '@remix-run/node-fetch-server'
-
-let handler: FetchHandler = async (request, client) => {
- // Log client information
- console.log(`Request from ${client.address}:${client.port}`)
-
- // Use for rate limiting, geolocation, etc.
- if (isRateLimited(client.address)) {
- return new Response('Too Many Requests', { status: 429 })
- }
-
- return Response.json({
- message: 'Hello!',
- yourIp: client.address,
- })
-}
-```
-
-## HTTPS support
-
-```ts
-import * as https from 'node:https'
-import * as fs from 'node:fs'
-import { createRequestListener } from '@remix-run/node-fetch-server'
-
-let options = {
- key: fs.readFileSync('private-key.pem'),
- cert: fs.readFileSync('certificate.pem'),
-}
-
-let server = https.createServer(options, createRequestListener(handler))
-
-server.listen(443, () => {
- console.log('HTTPS Server running on port 443')
-})
-```
-
-## Navigation
-
-- [node-fetch-server overview](./index.md)
-- [Advanced usage](./advanced-usage.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/remix.md b/docs/agents/remix/remix.md
deleted file mode 100644
index 30fc7d9..0000000
--- a/docs/agents/remix/remix.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# remix
-
-Source: https://github.com/remix-run/remix/tree/main/packages/remix
-
-## README
-
-A modern web framework for JavaScript.
-
-See [remix.run](https://remix.run) for framework docs.
-
-## Installation
-
-```sh
-npm i remix
-```
-
-## Package usage in Remix 3 alpha
-
-The `remix` package is used through subpath imports.
-
-- ✅ `import { createRouter } from 'remix/fetch-router'`
-- ✅ `import { route } from 'remix/fetch-router/routes'`
-- ✅ `import { createRoot } from 'remix/component'`
-- ❌ `import { ... } from 'remix'` (root import removed in `3.0.0-alpha.3`)
-
-## Subpath export surface (`3.0.0-alpha.3`)
-
-Top-level package exports currently include:
-
-- `remix/async-context-middleware`
-- `remix/component`
-- `remix/compression-middleware`
-- `remix/cookie`
-- `remix/data-schema`
-- `remix/data-table`
-- `remix/fetch-proxy`
-- `remix/fetch-router`
-- `remix/file-storage`
-- `remix/file-storage-s3`
-- `remix/form-data-middleware`
-- `remix/form-data-parser`
-- `remix/fs`
-- `remix/headers`
-- `remix/html-template`
-- `remix/interaction`
-- `remix/lazy-file`
-- `remix/logger-middleware`
-- `remix/method-override-middleware`
-- `remix/mime`
-- `remix/multipart-parser`
-- `remix/node-fetch-server`
-- `remix/response`
-- `remix/route-pattern`
-- `remix/session`
-- `remix/session-middleware`
-- `remix/session-storage-memcache`
-- `remix/session-storage-redis`
-- `remix/static-middleware`
-- `remix/tar-parser`
-
-Plus adapter/data helper subpaths and utility subpaths:
-
-- `remix/data-schema/checks`, `remix/data-schema/coerce`,
- `remix/data-schema/lazy`
-- `remix/data-table-mysql`, `remix/data-table-postgres`,
- `remix/data-table-sqlite`
-- `remix/fetch-router/routes`
-- `remix/component/jsx-runtime`, `remix/component/jsx-dev-runtime`,
- `remix/component/server`
-- `remix/interaction/form`, `remix/interaction/keys`,
- `remix/interaction/popover`, `remix/interaction/press`
-- `remix/response/compress`, `remix/response/file`, `remix/response/html`,
- `remix/response/redirect`
-- `remix/route-pattern/specificity`
-- `remix/session/cookie-storage`, `remix/session/fs-storage`,
- `remix/session/memory-storage`
-- `remix/file-storage/fs`, `remix/file-storage/memory`
-- `remix/multipart-parser/node`
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/remix/README.md b/docs/agents/remix/remix/README.md
new file mode 100644
index 0000000..46c8502
--- /dev/null
+++ b/docs/agents/remix/remix/README.md
@@ -0,0 +1,15 @@
+# remix
+
+A modern web framework for JavaScript.
+
+See [remix.run](https://remix.run) for more information.
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/response/README.md b/docs/agents/remix/response/README.md
new file mode 100644
index 0000000..57fc86e
--- /dev/null
+++ b/docs/agents/remix/response/README.md
@@ -0,0 +1,274 @@
+# response
+
+Response helper utilities for the web Fetch API. `response` provides focused
+helpers for common HTTP responses with correct headers and caching semantics.
+
+## Features
+
+- **Web Standards Compliant:** Built on the standard `Response` API, works in
+ any JavaScript runtime (Node.js, Bun, Deno, Cloudflare Workers)
+- [**File Responses:**](#file-responses) Full HTTP semantics including ETags,
+ Last-Modified, conditional requests, and Range support
+- [**HTML Responses:**](#html-responses) Automatic DOCTYPE prepending and proper
+ Content-Type headers
+- [**Redirect Responses:**](#redirect-responses) Simple redirect creation with
+ customizable status codes
+- [**Compress Responses:**](#compress-responses) Streaming compression based on
+ Accept-Encoding header
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Usage
+
+This package provides no default export. Instead, import the specific helper you
+need:
+
+```ts
+import { createFileResponse } from 'remix/response/file'
+import { createHtmlResponse } from 'remix/response/html'
+import { createRedirectResponse } from 'remix/response/redirect'
+import { compressResponse } from 'remix/response/compress'
+```
+
+### File Responses
+
+The `createFileResponse` helper creates a response for serving files with full
+HTTP semantics. It works with both native `File` objects and `LazyFile` from
+`@remix-run/lazy-file`:
+
+```ts
+import { createFileResponse } from 'remix/response/file'
+import { openLazyFile } from 'remix/fs'
+
+let lazyFile = openLazyFile('./public/image.jpg')
+let response = await createFileResponse(lazyFile, request, {
+ cacheControl: 'public, max-age=3600',
+})
+```
+
+#### Features
+
+- **Content-Type** and **Content-Length** headers
+- **ETag** generation (weak or strong)
+- **Last-Modified** headers
+- **Cache-Control** headers
+- **Conditional requests** (`If-None-Match`, `If-Modified-Since`, `If-Match`,
+ `If-Unmodified-Since`)
+- **Range requests** for partial content (`206 Partial Content`)
+- **HEAD** request support
+
+#### Options
+
+```ts
+await createFileResponse(file, request, {
+ // Cache-Control header value.
+ // Defaults to `undefined` (no Cache-Control header).
+ cacheControl: 'public, max-age=3600',
+
+ // ETag generation strategy:
+ // - 'weak': Generates weak ETags based on file size and mtime (default)
+ // - 'strong': Generates strong ETags by hashing file content
+ // - false: Disables ETag generation
+ etag: 'weak',
+
+ // Hash algorithm for strong ETags (Web Crypto API algorithm names).
+ // Only used when etag: 'strong'.
+ // Defaults to 'SHA-256'.
+ digest: 'SHA-256',
+
+ // Whether to generate Last-Modified headers.
+ // Defaults to `true`.
+ lastModified: true,
+
+ // Whether to support HTTP Range requests for partial content.
+ // Defaults to `true`.
+ acceptRanges: true,
+})
+```
+
+#### Strong ETags and Content Hashing
+
+For assets that require strong validation (e.g., to support
+[`If-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match)
+preconditions or
+[`If-Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range)
+with
+[`Range` requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)),
+configure strong ETag generation:
+
+```ts
+return createFileResponse(file, request, {
+ etag: 'strong',
+})
+```
+
+By default, strong ETags are generated using the Web Crypto API with the
+`'SHA-256'` algorithm. You can customize this:
+
+```ts
+return createFileResponse(file, request, {
+ etag: 'strong',
+ // Specify a different hash algorithm
+ digest: 'SHA-512',
+})
+```
+
+For large files or custom hashing requirements, provide a custom digest
+function:
+
+```ts
+await createFileResponse(file, request, {
+ etag: 'strong',
+ async digest(file) {
+ // Custom streaming hash for large files
+ let { createHash } = await import('node:crypto')
+ let hash = createHash('sha256')
+ for await (let chunk of file.stream()) {
+ hash.update(chunk)
+ }
+ return hash.digest('hex')
+ },
+})
+```
+
+### HTML Responses
+
+The `createHtmlResponse` helper creates HTML responses with proper
+`Content-Type` and DOCTYPE handling:
+
+```ts
+import { createHtmlResponse } from 'remix/response/html'
+
+let response = createHtmlResponse('Hello, World!
')
+// Content-Type: text/html; charset=UTF-8
+// Body: Hello, World!
+```
+
+The helper automatically prepends `` if not already present. It
+works with strings, `SafeHtml`
+[from `@remix-run/html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template),
+Blobs/Files, ArrayBuffers, and ReadableStreams.
+
+```ts
+import { html } from 'remix/html-template'
+import { createHtmlResponse } from 'remix/response/html'
+
+let name = ''
+let response = createHtmlResponse(html`Hello, ${name}!
`)
+// Safely escaped HTML
+```
+
+### Redirect Responses
+
+The `createRedirectResponse` helper creates redirect responses. The main
+improvements over the native `Response.redirect` API are:
+
+- Accepts a relative `location` instead of a full URL. This isn't technically
+ spec-compliant, but it's so widespread that many applications use relative
+ redirects regularly without issues.
+- Accepts a `ResponseInit` object as the second argument, allowing you to set
+ additional headers and status code.
+
+```ts
+import { createRedirectResponse } from 'remix/response/redirect'
+
+// Default 302 redirect
+let response = createRedirectResponse('/login')
+
+// Custom status code
+let response = createRedirectResponse('/new-page', 301)
+
+// With additional headers
+let response = createRedirectResponse('/dashboard', {
+ status: 303,
+ headers: { 'X-Redirect-Reason': 'authentication' },
+})
+```
+
+### Compress Responses
+
+The `compressResponse` helper compresses a `Response` based on the client's
+`Accept-Encoding` header:
+
+```ts
+import { compressResponse } from 'remix/response/compress'
+
+let response = new Response(JSON.stringify(data), {
+ headers: { 'Content-Type': 'application/json' },
+})
+let compressed = await compressResponse(response, request)
+```
+
+Compression is automatically skipped for:
+
+- Responses with no `Accept-Encoding` header
+- Responses that are already compressed (existing `Content-Encoding`)
+- Responses with `Cache-Control: no-transform`
+- Responses with `Content-Length` below threshold (default: 1024 bytes)
+- Responses with range support (`Accept-Ranges: bytes`)
+- 206 Partial Content responses
+- HEAD requests (only headers are modified)
+
+#### Options
+
+The `compressResponse` helper accepts options to customize compression behavior:
+
+```ts
+await compressResponse(response, request, {
+ // Minimum size in bytes to compress (only enforced if Content-Length is present).
+ // Default: 1024
+ threshold: 1024,
+
+ // Which encodings the server supports for negotiation.
+ // Defaults to ['br', 'gzip', 'deflate']
+ encodings: ['br', 'gzip', 'deflate'],
+
+ // node:zlib options for gzip/deflate compression.
+ // For SSE responses (text/event-stream), flush: Z_SYNC_FLUSH
+ // is automatically applied unless you explicitly set a flush value.
+ // See: https://nodejs.org/api/zlib.html#class-options
+ zlib: {
+ level: 6,
+ },
+
+ // node:zlib options for Brotli compression.
+ // For SSE responses (text/event-stream), flush: BROTLI_OPERATION_FLUSH
+ // is automatically applied unless you explicitly set a flush value.
+ // See: https://nodejs.org/api/zlib.html#class-brotlioptions
+ brotli: {
+ params: {
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4,
+ },
+ },
+})
+```
+
+#### Range Requests and Compression
+
+Range requests and compression are mutually exclusive. When
+`Accept-Ranges: bytes` is present in the response headers, `compressResponse`
+will not compress the response. This is why the `createFileResponse` helper
+enables ranges only for non-compressible MIME types by default - to allow
+text-based assets to be compressed while still supporting resumable downloads
+for media files.
+
+## Related Packages
+
+- [`@remix-run/headers`](https://github.com/remix-run/remix/tree/main/packages/headers) -
+ Type-safe HTTP header manipulation
+- [`@remix-run/html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template) -
+ Safe HTML templating with automatic escaping
+- [`@remix-run/fs`](https://github.com/remix-run/remix/tree/main/packages/fs) -
+ File system utilities including `openFile`
+- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
+ Build HTTP routers using the web fetch API
+- [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) -
+ MIME type utilities
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/response/compress-responses.md b/docs/agents/remix/response/compress-responses.md
deleted file mode 100644
index 9573fdb..0000000
--- a/docs/agents/remix/response/compress-responses.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Compressed responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `compressResponse` helper compresses a `Response` based on the client's
-`Accept-Encoding` header:
-
-```ts
-import { compressResponse } from '@remix-run/response/compress'
-
-let response = new Response(JSON.stringify(data), {
- headers: { 'Content-Type': 'application/json' },
-})
-let compressed = await compressResponse(response, request)
-```
-
-Compression is automatically skipped for:
-
-- Responses with no `Accept-Encoding` header
-- Responses that are already compressed (existing `Content-Encoding`)
-- Responses with `Cache-Control: no-transform`
-- Responses with `Content-Length` below threshold (default: 1024 bytes)
-- Responses with range support (`Accept-Ranges: bytes`)
-- 206 Partial Content responses
-- HEAD requests (only headers are modified)
-
-## Options
-
-```ts
-await compressResponse(response, request, {
- // Minimum size in bytes to compress (only enforced if Content-Length is present).
- // Default: 1024
- threshold: 1024,
-
- // Which encodings the server supports for negotiation.
- // Defaults to ['br', 'gzip', 'deflate']
- encodings: ['br', 'gzip', 'deflate'],
-
- // node:zlib options for gzip/deflate compression.
- // For SSE responses (text/event-stream), flush: Z_SYNC_FLUSH
- // is automatically applied unless you explicitly set a flush value.
- // See: https://nodejs.org/api/zlib.html#class-options
- zlib: {
- level: 6,
- },
-
- // node:zlib options for Brotli compression.
- // For SSE responses (text/event-stream), flush: BROTLI_OPERATION_FLUSH
- // is automatically applied unless you explicitly set a flush value.
- // See: https://nodejs.org/api/zlib.html#class-brotlioptions
- brotli: {
- params: {
- [zlib.constants.BROTLI_PARAM_QUALITY]: 4,
- },
- },
-})
-```
-
-## Range requests and compression
-
-Range requests and compression are mutually exclusive. When
-`Accept-Ranges: bytes` is present in the response headers, `compressResponse`
-will not compress the response. This is why the `createFileResponse` helper
-enables ranges only for non-compressible MIME types by default - to allow
-text-based assets to be compressed while still supporting resumable downloads
-for media files.
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/file-responses.md b/docs/agents/remix/response/file-responses.md
deleted file mode 100644
index 43d7ddc..0000000
--- a/docs/agents/remix/response/file-responses.md
+++ /dev/null
@@ -1,108 +0,0 @@
-# File responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `createFileResponse` helper creates a response for serving files with full
-HTTP semantics. It works with both native `File` objects and `LazyFile` from
-`@remix-run/lazy-file`.
-
-```ts
-import { createFileResponse } from '@remix-run/response/file'
-import { openLazyFile } from '@remix-run/fs'
-
-let lazyFile = openLazyFile('./public/image.jpg')
-let response = await createFileResponse(lazyFile, request, {
- cacheControl: 'public, max-age=3600',
-})
-```
-
-## Features
-
-- **Content-Type** and **Content-Length** headers
-- **ETag** generation (weak or strong)
-- **Last-Modified** headers
-- **Cache-Control** headers
-- **Conditional requests** (`If-None-Match`, `If-Modified-Since`, `If-Match`,
- `If-Unmodified-Since`)
-- **Range requests** for partial content (`206 Partial Content`)
-- **HEAD** request support
-
-## Options
-
-```ts
-await createFileResponse(file, request, {
- // Cache-Control header value.
- // Defaults to `undefined` (no Cache-Control header).
- cacheControl: 'public, max-age=3600',
-
- // ETag generation strategy:
- // - 'weak': Generates weak ETags based on file size and mtime (default)
- // - 'strong': Generates strong ETags by hashing file content
- // - false: Disables ETag generation
- etag: 'weak',
-
- // Hash algorithm for strong ETags (Web Crypto API algorithm names).
- // Only used when etag: 'strong'.
- // Defaults to 'SHA-256'.
- digest: 'SHA-256',
-
- // Whether to generate Last-Modified headers.
- // Defaults to `true`.
- lastModified: true,
-
- // Whether to support HTTP Range requests for partial content.
- // Defaults to `true`.
- acceptRanges: true,
-})
-```
-
-## Strong ETags and content hashing
-
-For assets that require strong validation (e.g., to support
-[`If-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match)
-preconditions or
-[`If-Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range)
-with
-[`Range` requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)),
-configure strong ETag generation:
-
-```ts
-return createFileResponse(file, request, {
- etag: 'strong',
-})
-```
-
-By default, strong ETags are generated using the Web Crypto API with the
-`'SHA-256'` algorithm. You can customize this:
-
-```ts
-return createFileResponse(file, request, {
- etag: 'strong',
- // Specify a different hash algorithm
- digest: 'SHA-512',
-})
-```
-
-For large files or custom hashing requirements, provide a custom digest
-function:
-
-```ts
-await createFileResponse(file, request, {
- etag: 'strong',
- async digest(file) {
- // Custom streaming hash for large files
- let { createHash } = await import('node:crypto')
- let hash = createHash('sha256')
- for await (let chunk of file.stream()) {
- hash.update(chunk)
- }
- return hash.digest('hex')
- },
-})
-```
-
-## Navigation
-
-- [Response overview](./index.md)
-- [HTML responses](./html-responses.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/html-responses.md b/docs/agents/remix/response/html-responses.md
deleted file mode 100644
index 27ff0f0..0000000
--- a/docs/agents/remix/response/html-responses.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# HTML responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `createHtmlResponse` helper creates HTML responses with proper
-`Content-Type` and DOCTYPE handling:
-
-```ts
-import { createHtmlResponse } from '@remix-run/response/html'
-
-let response = createHtmlResponse('Hello, World!
')
-// Content-Type: text/html; charset=UTF-8
-// Body: Hello, World!
-```
-
-The helper automatically prepends `` if not already present. It
-works with strings, `SafeHtml` from `@remix-run/html-template`, Blobs/Files,
-ArrayBuffers, and ReadableStreams.
-
-```ts
-import { html } from '@remix-run/html-template'
-import { createHtmlResponse } from '@remix-run/response/html'
-
-let name = ''
-let response = createHtmlResponse(html`Hello, ${name}!
`)
-// Safely escaped HTML
-```
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Redirect responses](./redirect-responses.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/index.md b/docs/agents/remix/response/index.md
deleted file mode 100644
index eb5fedc..0000000
--- a/docs/agents/remix/response/index.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# response
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-## Overview
-
-Response helpers for the web Fetch API. `response` provides a collection of
-helper functions for creating common HTTP responses with proper headers and
-semantics.
-
-## Features
-
-- **Web Standards Compliant:** Built on the standard `Response` API, works in
- any JavaScript runtime (Node.js, Bun, Deno, Cloudflare Workers)
-- **File Responses:** Full HTTP semantics including ETags, Last-Modified,
- conditional requests, and Range support
-- **HTML Responses:** Automatic DOCTYPE prepending and proper Content-Type
- headers
-- **Redirect Responses:** Simple redirect creation with customizable status
- codes
-- **Compress Responses:** Streaming compression based on Accept-Encoding header
-
-## Installation
-
-```sh
-bun add @remix-run/response
-```
-
-## Usage
-
-This package provides no default export. Instead, import the specific helper you
-need:
-
-```ts
-import { createFileResponse } from '@remix-run/response/file'
-import { createHtmlResponse } from '@remix-run/response/html'
-import { createRedirectResponse } from '@remix-run/response/redirect'
-import { compressResponse } from '@remix-run/response/compress'
-```
-
-## Navigation
-
-- [File responses](./file-responses.md)
-- [HTML responses](./html-responses.md)
-- [Redirect responses](./redirect-responses.md)
-- [Compressed responses](./compress-responses.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/redirect-responses.md b/docs/agents/remix/response/redirect-responses.md
deleted file mode 100644
index f5f58f5..0000000
--- a/docs/agents/remix/response/redirect-responses.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Redirect responses
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-The `createRedirectResponse` helper creates redirect responses. The main
-improvements over the native `Response.redirect` API are:
-
-- Accepts a relative `location` instead of a full URL.
-- Accepts a `ResponseInit` object as the second argument, allowing you to set
- additional headers and status code.
-
-```ts
-import { createRedirectResponse } from '@remix-run/response/redirect'
-
-// Default 302 redirect
-let response = createRedirectResponse('/login')
-
-// Custom status code
-let response = createRedirectResponse('/new-page', 301)
-
-// With additional headers
-let response = createRedirectResponse('/dashboard', {
- status: 303,
- headers: { 'X-Redirect-Reason': 'authentication' },
-})
-```
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Compressed responses](./compress-responses.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/response/related.md b/docs/agents/remix/response/related.md
deleted file mode 100644
index 9e1eabf..0000000
--- a/docs/agents/remix/response/related.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Related packages
-
-Source: https://github.com/remix-run/remix/tree/main/packages/response
-
-## Related packages
-
-- [`@remix-run/headers`](https://github.com/remix-run/remix/tree/main/packages/headers) -
- Type-safe HTTP header manipulation
-- [`@remix-run/html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template) -
- Safe HTML templating with automatic escaping
-- [`@remix-run/fs`](https://github.com/remix-run/remix/tree/main/packages/fs) -
- File system utilities including `openFile`
-- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Build HTTP routers using the web fetch API
-- [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) -
- MIME type utilities
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Response overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/route-pattern.md b/docs/agents/remix/route-pattern/README.md
similarity index 74%
rename from docs/agents/remix/route-pattern.md
rename to docs/agents/remix/route-pattern/README.md
index a8fafa9..ed8dc32 100644
--- a/docs/agents/remix/route-pattern.md
+++ b/docs/agents/remix/route-pattern/README.md
@@ -1,13 +1,31 @@
# route-pattern
-Source: https://github.com/remix-run/remix/tree/main/packages/route-pattern
+Type-safe URL matching and href generation for JavaScript. `route-pattern`
+supports path params, wildcards, optionals, and full-URL patterns with
+predictable ranking.
+
+## Features
+
+- **Type-Safe Params** - Infer params from patterns for compile-time route
+ correctness
+- **Flexible Pattern Syntax** - Variables, wildcards, optionals, and query
+ constraints
+- **Full URL Support** - Match protocol, host, pathname, and search params
+- **Deterministic Ranking** - Static segments beat params, and params beat
+ wildcards
+- **Runtime Agnostic** - Works across Node.js, Bun, Deno, Cloudflare Workers,
+ and browsers
-## README
+## Installation
+
+```sh
+npm i remix
+```
-Fast URL matching and href generation with type safe params.
+## Quick Example
```ts
-import { RoutePattern } from '@remix-run/route-pattern'
+import { RoutePattern } from 'remix/route-pattern'
let blog = new RoutePattern('blog/:slug')
blog.match('https://remix.run/blog/v3') // { params: { slug: 'v3' } }
@@ -23,22 +41,6 @@ cdn.match('https://us-west.cdn.com/assets/images/logo.png') // { params: { regio
cdn.href({ region: 'us-west', file: 'images/logo', ext: 'png' }) // 'https://us-west.cdn.com/assets/images/logo.png'
```
-**Goals**
-
-- **Universal**: Runs on any JS runtime (Node, Bun, Deno, Cloudflare Workers,
- browsers, ...)
-- **Type-safe params**: Autocomplete and validation for variables, wildcards,
- and search params
-- **Full URL matching**: Protocol, hostname, port, pathname, search params
-- **Fast**: Includes matchers optimized for small and large apps
-- **Simple ranking**: Static segments beat variables, variables beat wildcards
-
-## Installation
-
-```sh
-bun add @remix-run/route-pattern
-```
-
## Intuitive syntax
**Variables** capture dynamic segments using `:name`:
@@ -65,11 +67,14 @@ new RoutePattern('docs(/guides/:category)') // multiple segments optional: /docs
new RoutePattern('api(/v:major(.:minor))') // nested optionals: /api, /api/v2, /api/v2.1
```
-**Search params** narrow matches using `?key` or `?key=value`:
+**Search params** narrow matches using `?key`, `?key=`, or `?key=value`. Parsing
+and serialization follow `URLSearchParams`
+(`application/x-www-form-urlencoded`): `?key` and `?key=` are the same
+constraint (stored as an empty `Set` in `ast.search`: key must be present; empty
+value is OK), and spaces use `+` / `%20` like in real query strings.
```ts
-new RoutePattern('search?q') // requires ?q in URL
-new RoutePattern('search?q=') // requires ?q with any value
+new RoutePattern('search?q') // same constraint as ?q= — key must be present
new RoutePattern('search?q=routing') // requires ?q=routing exactly
```
@@ -87,9 +92,9 @@ Match URLs against multiple patterns. Each pattern can have associated data
(handlers, route IDs, metadata, etc.):
```ts
-import { ArrayMatcher as Matcher } from '@remix-run/route-pattern'
+import { ArrayMatcher as Matcher } from 'remix/route-pattern'
-// Any data type you want!
+// Any data type you want! 👇
let matcher = new Matcher()
matcher.add('/', 'home')
@@ -108,14 +113,14 @@ matcher.match('https://example.com/api/v2/users/profile')
- **ArrayMatcher**: Best for small apps (~80 routes or fewer)
- **TrieMatcher**: Best for large apps (hundreds of routes)
-Note: Performance depends on your specific patterns - benchmark both to verify
+Note: Performance depends on your specific patterns—benchmark both to verify
which is faster for your app.
Both implement the `Matcher` API so you can swap them out easily:
```ts
-// import { ArrayMatcher as Matcher } from "@remix-run/route-pattern"
-import { TrieMatcher as Matcher } from '@remix-run/route-pattern'
+// import { ArrayMatcher as Matcher } from 'remix/route-pattern'
+import { TrieMatcher as Matcher } from 'remix/route-pattern'
```
## Specificity
@@ -125,7 +130,7 @@ When multiple patterns match a URL, the most specific pattern wins.
**Pathname specificity** (left-to-right):
```ts
-import { ArrayMatcher } from '@remix-run/route-pattern'
+import { ArrayMatcher } from 'remix/route-pattern'
let matcher = new ArrayMatcher()
matcher.add('blog/hello', 'static')
@@ -144,12 +149,11 @@ matcher.match('https://example.com/blog/hello')
let router = new ArrayMatcher()
router.add('search', 'no-params')
router.add('search?q', 'has-q')
-router.add('search?q=', 'has-q-with-value')
router.add('search?q=hello', 'exact-match')
router.match('https://example.com/search?q=hello')
// { pattern: 'search?q=hello', params: {}, data: 'exact-match' }
-// More constrained search params = more specific
+// More constrained search params = more specific (`?q` and `?q=` tie)
```
## Benchmark
@@ -169,7 +173,3 @@ pnpm bench bench/comparison.bench.ts
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session-middleware.md b/docs/agents/remix/session-middleware/README.md
similarity index 63%
rename from docs/agents/remix/session-middleware.md
rename to docs/agents/remix/session-middleware/README.md
index 70a1d76..cd7bce3 100644
--- a/docs/agents/remix/session-middleware.md
+++ b/docs/agents/remix/session-middleware/README.md
@@ -1,26 +1,29 @@
# session-middleware
-Source: https://github.com/remix-run/remix/tree/main/packages/session-middleware
+Session middleware for Remix using signed cookies. It loads session state from
+incoming requests, stores it in request context using `Session`, and persists
+updates automatically.
-## README
+## Features
-Middleware for managing sessions with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router)
-via securely signed cookies.
+- **Session Lifecycle Handling** - Reads and saves session state per request
+- **Context Integration** - Exposes session APIs directly on request context
+- **Secure Cookie Support** - Designed for signed session cookies
## Installation
```sh
-bun add @remix-run/session-middleware
+npm i remix
```
## Usage
```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { createCookie } from '@remix-run/cookie'
-import { createCookieSessionStorage } from '@remix-run/session/cookie-storage'
-import { session } from '@remix-run/session-middleware'
+import { createRouter } from 'remix/fetch-router'
+import { createCookie } from 'remix/cookie'
+import { Session } from 'remix/session'
+import { createCookieSessionStorage } from 'remix/session/cookie-storage'
+import { session } from 'remix/session-middleware'
let sessionCookie = createCookie('__session', {
secrets: ['s3cr3t'], // session cookies must be signed!
@@ -36,15 +39,16 @@ let router = createRouter({
})
router.get('/', (context) => {
- context.session.set('count', Number(context.session.get('count') ?? 0) + 1)
- return new Response(`Count: ${context.session.get('count')}`)
+ let session = context.get(Session)
+ session.set('count', Number(session.get('count') ?? 0) + 1)
+ return new Response(`Count: ${session.get('count')}`)
})
```
The middleware:
- Reads the session from the cookie on incoming requests
-- Makes it available as `context.session`
+- Makes it available as `context.get(Session)`
- Automatically saves session changes and sets the cookie on responses
Note: The session cookie must be signed for security. This prevents tampering
@@ -55,9 +59,11 @@ with the session data on the client.
A basic login/logout flow could look like this:
```ts
-import * as res from '@remix-run/fetch-router/response-helpers'
+import * as res from 'remix/fetch-router/response-helpers'
+import { Session } from 'remix/session'
-router.get('/login', ({ session }) => {
+router.get('/login', ({ get }) => {
+ let session = get(Session)
let error = session.get('error')
return res.html(`
@@ -74,7 +80,9 @@ router.get('/login', ({ session }) => {
`)
})
-router.post('/login', ({ session, formData }) => {
+router.post('/login', ({ get }) => {
+ let session = get(Session)
+ let formData = get(FormData)
let username = formData.get('username')
let password = formData.get('password')
@@ -90,7 +98,8 @@ router.post('/login', ({ session, formData }) => {
return res.redirect('/dashboard')
})
-router.post('/logout', ({ session }) => {
+router.post('/logout', ({ get }) => {
+ let session = get(Session)
session.destroy()
return res.redirect('/')
})
@@ -108,7 +117,3 @@ router.post('/logout', ({ session }) => {
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session-storage-memcache.md b/docs/agents/remix/session-storage-memcache.md
deleted file mode 100644
index c2f8a4b..0000000
--- a/docs/agents/remix/session-storage-memcache.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# session-storage-memcache
-
-Source:
-https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache
-
-## README
-
-Memcache session storage for `remix/session`.
-
-## Installation
-
-```sh
-npm i remix
-```
-
-## Usage
-
-```ts
-import { createMemcacheSessionStorage } from 'remix/session-storage-memcache'
-
-let sessionStorage = createMemcacheSessionStorage('127.0.0.1:11211', {
- keyPrefix: 'my-app:session:',
- ttlSeconds: 60 * 60 * 24 * 7,
-})
-```
-
-## Options
-
-- `useUnknownIds` (`boolean`, default: `false`)
-- `keyPrefix` (`string`, default: `'remix:session:'`)
-- `ttlSeconds` (`number`, default: `0`)
-
-Memcache storage uses TCP sockets and therefore requires a Node.js runtime.
-
-## Related packages
-
-- [`session`](https://github.com/remix-run/remix/tree/main/packages/session)
-- [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware)
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session-storage-memcache/README.md b/docs/agents/remix/session-storage-memcache/README.md
new file mode 100644
index 0000000..6e7ded0
--- /dev/null
+++ b/docs/agents/remix/session-storage-memcache/README.md
@@ -0,0 +1,42 @@
+# session-storage-memcache
+
+Memcache session storage for
+[`@remix-run/session`](https://github.com/remix-run/remix/tree/main/packages/session).
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Usage
+
+```ts
+import { createMemcacheSessionStorage } from 'remix/session-storage-memcache'
+
+let sessionStorage = createMemcacheSessionStorage('127.0.0.1:11211', {
+ keyPrefix: 'my-app:session:',
+ ttlSeconds: 60 * 60 * 24 * 7,
+})
+```
+
+Available options:
+
+- `useUnknownIds` (default: `false`) - reuse unknown session IDs sent by the
+ client
+- `keyPrefix` (default: `'remix:session:'`) - prefix for all Memcache keys
+- `ttlSeconds` (default: `0`) - session expiration in seconds (`0` means no
+ expiration)
+
+Note: Memcache storage uses TCP sockets and requires a Node.js runtime.
+
+## Related Packages
+
+- [`@remix-run/session`](https://github.com/remix-run/remix/tree/main/packages/session) -
+ Core session primitives and storage interface
+- [`@remix-run/session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) -
+ Middleware for wiring session storage into request handling
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/session-storage-redis.md b/docs/agents/remix/session-storage-redis/README.md
similarity index 53%
rename from docs/agents/remix/session-storage-redis.md
rename to docs/agents/remix/session-storage-redis/README.md
index 96534c4..835fcdc 100644
--- a/docs/agents/remix/session-storage-redis.md
+++ b/docs/agents/remix/session-storage-redis/README.md
@@ -1,23 +1,20 @@
# session-storage-redis
-Source:
-https://github.com/remix-run/remix/tree/main/packages/session-storage-redis
-
-## README
-
-Redis-backed session storage for `remix/session`.
+Redis-backed session storage for
+[`@remix-run/session`](https://github.com/remix-run/remix/tree/main/packages/session).
+Use this package when app servers need to share session state through Redis.
## Installation
```sh
-npm i remix redis
+npm i @remix-run/session @remix-run/session-storage-redis redis
```
## Usage
```ts
import { createClient } from 'redis'
-import { createRedisSessionStorage } from 'remix/session-storage-redis'
+import { createRedisSessionStorage } from '@remix-run/session-storage-redis'
let redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
@@ -30,14 +27,12 @@ let sessionStorage = createRedisSessionStorage(redis, {
## Options
+`createRedisSessionStorage(client, options)` supports:
+
- `keyPrefix` (`string`, default: `'session:'`)
-- `ttl` (`number` in seconds)
+- `ttl` (`number` seconds)
- `useUnknownIds` (`boolean`, default: `false`)
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/session/README.md b/docs/agents/remix/session/README.md
new file mode 100644
index 0000000..bf5256e
--- /dev/null
+++ b/docs/agents/remix/session/README.md
@@ -0,0 +1,204 @@
+# session
+
+A session management library for JavaScript. This package provides a flexible
+and secure way to manage user sessions in server-side applications with a
+flexible API for different session storage strategies.
+
+## Features
+
+- **Multiple Storage Strategies:** Includes memory, cookie, and file-based
+ [session storage strategies](#storage-strategies) for different use cases
+- **Flash Messages:** Support for [flash data](#flash-messages) that persists
+ only for the next request
+- **Session Security:** Built-in protection against
+ [session fixation attacks](#regenerating-session-ids)
+
+## Installation
+
+```sh
+npm i remix
+```
+
+## Usage
+
+The following example shows how to use a session to persist data across
+requests.
+
+The standard pattern when working with sessions is to read the session from the
+request, modify it, and save it back to storage and write the session cookie to
+the response.
+
+```ts
+import { createCookieSessionStorage } from 'remix/session/cookie-storage'
+
+// Create a session storage. This is used to store session data across requests.
+let storage = createCookieSessionStorage()
+
+// This function simulates a typical request flow where the session is read from
+// the request cookie, modified, and the new cookie is returned in the response.
+async function handleRequest(cookie: string | null) {
+ let session = await storage.read(cookie)
+ session.set('count', Number(session.get('count') ?? 0) + 1)
+ return {
+ session, // The session data from this "request"
+ cookie: await storage.save(session), // The cookie to use on the next request
+ }
+}
+
+let response1 = await handleRequest(null)
+assert.equal(response1.session.get('count'), 1)
+
+let response2 = await handleRequest(response1.cookie)
+assert.equal(response2.session.get('count'), 2)
+
+let response3 = await handleRequest(response2.cookie)
+assert.equal(response3.session.get('count'), 3)
+```
+
+The example above is a low-level illustration of how to use this package for
+session management. In practice, you would use the `session` middleware in
+[`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router)
+to automatically manage the session for you.
+
+### Flash Messages
+
+Flash messages are values that persist only for the next request, perfect for
+displaying one-time notifications:
+
+```ts
+async function requestIndex(cookie: string | null) {
+ let session = await storage.read(cookie)
+ return { session, cookie: await storage.save(session) }
+}
+
+async function requestSubmit(cookie: string | null) {
+ let session = await storage.read(cookie)
+ session.flash('message', 'success!')
+ return { session, cookie: await storage.save(session) }
+}
+
+// Flash data is undefined on the first request
+let response1 = await requestIndex(null)
+assert.equal(response1.session.get('message'), undefined)
+
+// Flash data is undefined on the same request it is set. This response
+// is typically a redirect to a route that displays the flash data.
+let response2 = await requestSubmit(response1.cookie)
+assert.equal(response2.session.get('message'), undefined)
+
+// Flash data is available on the next request
+let response3 = await requestIndex(response2.cookie)
+assert.equal(response3.session.get('message'), 'success!')
+
+// Flash data is not available on subsequent requests
+let response4 = await requestIndex(response3.cookie)
+assert.equal(response4.session.get('message'), undefined)
+```
+
+### Regenerating Session IDs
+
+For security, regenerate the session ID after privilege changes like a login.
+This helps prevent session fixation attacks by issuing a new session ID in the
+response.
+
+```ts
+import { createFsSessionStorage } from 'remix/session/fs-storage'
+
+let sessionStorage = createFsSessionStorage('/tmp/sessions')
+
+async function requestIndex(cookie: string | null) {
+ let session = await sessionStorage.read(cookie)
+ return { session, cookie: await sessionStorage.save(session) }
+}
+
+async function requestLogin(cookie: string | null) {
+ let session = await sessionStorage.read(cookie)
+ session.set('userId', 'mj')
+ session.regenerateId()
+ return { session, cookie: await sessionStorage.save(session) }
+}
+
+let response1 = await requestIndex(null)
+assert.equal(response1.session.get('userId'), undefined)
+
+let response2 = await requestLogin(response1.cookie)
+assert.notEqual(response2.session.id, response1.session.id)
+
+let response3 = await requestIndex(response2.cookie)
+assert.equal(response3.session.get('userId'), 'mj')
+```
+
+To delete the old session data when the session is saved, use
+`session.regenerateId(true)`. This can help to prevent session fixation attacks
+by deleting the old session data when the session is saved. However, it may not
+be desirable in a situation with mobile clients on flaky connections that may
+need to resume the session using an old session ID.
+
+### Destroying Sessions
+
+When a user logs out, you should destroy the session using `session.destroy()`.
+
+This will clear all session data from storage the next time it is saved. It also
+clears the session ID on the client in the next response, so it will start with
+a new session on the next request.
+
+### Storage Strategies
+
+Several strategies are provided out of the box for storing session data across
+requests, depending on your needs.
+
+A session storage object must always be initialized with a _signed_ session
+cookie. This is used to identify the session and to store the session data in
+the response.
+
+#### Filesystem Storage
+
+Filesystem storage is a good choice for production environments. It requires
+access to a persistent filesystem, which is readily available on most servers.
+And it can scale to handle sessions with a lot of data easily.
+
+```ts
+import { createFsSessionStorage } from 'remix/session/fs-storage'
+
+let sessionStorage = createFsSessionStorage('/tmp/sessions')
+```
+
+#### Cookie Storage
+
+Cookie storage is suitable for production environments. In this strategy, all
+session data is stored directly in the session cookie itself, which means it
+doesn't require any additional storage.
+
+The main limitation of cookie storage is that the total size of the session
+cookie is limited to the browser's maximum cookie size, typically 4096 bytes.
+
+```ts
+import { createCookieSessionStorage } from 'remix/session/cookie-storage'
+
+let sessionStorage = createCookieSessionStorage()
+```
+
+#### Memory Storage
+
+Memory storage is useful in testing and development environments. In this
+strategy, all session data is stored in memory, which means no additional
+storage is required. However, all session data is lost when the server restarts.
+
+```ts
+import { createMemorySessionStorage } from 'remix/session/memory-storage'
+
+let sessionStorage = createMemorySessionStorage()
+```
+
+## Related Packages
+
+- [`@remix-run/cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) -
+ Cookie parsing and serialization
+- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
+ Router with built-in session middleware
+- [`@remix-run/session-storage-memcache`](https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache) -
+ Memcache-backed session storage
+
+## License
+
+See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
diff --git a/docs/agents/remix/session/flash-and-security.md b/docs/agents/remix/session/flash-and-security.md
deleted file mode 100644
index 0f71be4..0000000
--- a/docs/agents/remix/session/flash-and-security.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Flash data and security
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-## Flash messages
-
-Flash messages are values that persist only for the next request, perfect for
-displaying one-time notifications:
-
-```ts
-async function requestIndex(cookie: string | null) {
- let session = await storage.read(cookie)
- return { session, cookie: await storage.save(session) }
-}
-
-async function requestSubmit(cookie: string | null) {
- let session = await storage.read(cookie)
- session.flash('message', 'success!')
- return { session, cookie: await storage.save(session) }
-}
-
-// Flash data is undefined on the first request
-let response1 = await requestIndex(null)
-assert.equal(response1.session.get('message'), undefined)
-
-// Flash data is undefined on the same request it is set. This response
-// is typically a redirect to a route that displays the flash data.
-let response2 = await requestSubmit(response1.cookie)
-assert.equal(response2.session.get('message'), undefined)
-
-// Flash data is available on the next request
-let response3 = await requestIndex(response2.cookie)
-assert.equal(response3.session.get('message'), 'success!')
-
-// Flash data is not available on subsequent requests
-let response4 = await requestIndex(response3.cookie)
-assert.equal(response4.session.get('message'), undefined)
-```
-
-## Regenerating session IDs
-
-For security, regenerate the session ID after privilege changes like a login.
-This helps prevent session fixation attacks by issuing a new session ID in the
-response.
-
-```ts
-import { createFsSessionStorage } from '@remix-run/session/fs-storage'
-
-let sessionStorage = createFsSessionStorage('/tmp/sessions')
-
-async function requestIndex(cookie: string | null) {
- let session = await sessionStorage.read(cookie)
- return { session, cookie: await sessionStorage.save(session) }
-}
-
-async function requestLogin(cookie: string | null) {
- let session = await sessionStorage.read(cookie)
- session.set('userId', 'mj')
- session.regenerateId()
- return { session, cookie: await sessionStorage.save(session) }
-}
-
-let response1 = await requestIndex(null)
-assert.equal(response1.session.get('userId'), undefined)
-
-let response2 = await requestLogin(response1.cookie)
-assert.notEqual(response2.session.id, response1.session.id)
-
-let response3 = await requestIndex(response2.cookie)
-assert.equal(response3.session.get('userId'), 'mj')
-```
-
-To delete the old session data when the session is saved, use
-`session.regenerateId(true)`. This can help to prevent session fixation attacks
-by deleting the old session data when the session is saved. However, it may not
-be desirable in a situation with mobile clients on flaky connections that may
-need to resume the session using an old session ID.
-
-## Destroying sessions
-
-When a user logs out, you should destroy the session using `session.destroy()`.
-
-This will clear all session data from storage the next time it is saved. It also
-clears the session ID on the client in the next response, so it will start with
-a new session on the next request.
-
-## Navigation
-
-- [Session overview](./index.md)
-- [Storage strategies](./storage-strategies.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/session/index.md b/docs/agents/remix/session/index.md
deleted file mode 100644
index 6e758ad..0000000
--- a/docs/agents/remix/session/index.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# session
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-## Overview
-
-A full-featured session management library for JavaScript. This package provides
-a flexible and secure way to manage user sessions in server-side applications
-with a flexible API for different session storage strategies.
-
-## Features
-
-- **Multiple Storage Strategies:** Includes memory, cookie, and file-based
- storage strategies for different use cases
-- **Flash Messages:** Support for flash data that persists only for the next
- request
-- **Session Security:** Built-in protection against session fixation attacks
-
-## Installation
-
-```sh
-bun add @remix-run/session
-```
-
-## Usage
-
-The standard pattern is to read the session from the request, modify it, and
-save it back to storage and write the session cookie to the response.
-
-```ts
-import { createCookieSessionStorage } from '@remix-run/session/cookie-storage'
-
-// Create a session storage. This is used to store session data across requests.
-let storage = createCookieSessionStorage()
-
-// This function simulates a typical request flow where the session is read from
-// the request cookie, modified, and the new cookie is returned in the response.
-async function handleRequest(cookie: string | null) {
- let session = await storage.read(cookie)
- session.set('count', Number(session.get('count') ?? 0) + 1)
- return {
- session, // The session data from this "request"
- cookie: await storage.save(session), // The cookie to use on the next request
- }
-}
-
-let response1 = await handleRequest(null)
-assert.equal(response1.session.get('count'), 1)
-
-let response2 = await handleRequest(response1.cookie)
-assert.equal(response2.session.get('count'), 2)
-
-let response3 = await handleRequest(response2.cookie)
-assert.equal(response3.session.get('count'), 3)
-```
-
-The example above is a low-level illustration of how to use this package for
-session management. In practice, you would use the `session` middleware in
-[`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router)
-to automatically manage the session for you.
-
-## Navigation
-
-- [Flash data and security](./flash-and-security.md)
-- [Storage strategies](./storage-strategies.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/session/related.md b/docs/agents/remix/session/related.md
deleted file mode 100644
index 932ef56..0000000
--- a/docs/agents/remix/session/related.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Related packages
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-## Related packages
-
-- [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) -
- Cookie parsing and serialization
-- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) -
- Router with built-in session middleware
-- [`session-storage-memcache`](https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache) -
- Memcache-backed session storage adapter
-- [`session-storage-redis`](https://github.com/remix-run/remix/tree/main/packages/session-storage-redis) -
- Redis-backed session storage adapter
-
-## License
-
-See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Session overview](./index.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/session/storage-strategies.md b/docs/agents/remix/session/storage-strategies.md
deleted file mode 100644
index 2347c11..0000000
--- a/docs/agents/remix/session/storage-strategies.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Storage strategies
-
-Source: https://github.com/remix-run/remix/tree/main/packages/session
-
-Several strategies are provided out of the box for storing session data across
-requests, depending on your needs.
-
-A session storage object must always be initialized with a signed session
-cookie. This is used to identify the session and to store the session data in
-the response.
-
-## Filesystem storage
-
-Filesystem storage is a good choice for production environments. It requires
-access to a persistent filesystem, which is readily available on most servers.
-And it can scale to handle sessions with a lot of data easily.
-
-```ts
-import { createFsSessionStorage } from '@remix-run/session/fs-storage'
-
-let sessionStorage = createFsSessionStorage('/tmp/sessions')
-```
-
-## Cookie storage
-
-Cookie storage is suitable for production environments. In this strategy, all
-session data is stored directly in the session cookie itself, which means it
-doesn't require any additional storage.
-
-The main limitation of cookie storage is that the total size of the session
-cookie is limited to the browser's maximum cookie size, typically 4096 bytes.
-
-```ts
-import { createCookieSessionStorage } from '@remix-run/session/cookie-storage'
-
-let sessionStorage = createCookieSessionStorage()
-```
-
-## Memory storage
-
-Memory storage is useful in testing and development environments. In this
-strategy, all session data is stored in memory, which means no additional
-storage is required. However, all session data is lost when the server restarts.
-
-```ts
-import { createMemorySessionStorage } from '@remix-run/session/memory-storage'
-
-let sessionStorage = createMemorySessionStorage()
-```
-
-## Navigation
-
-- [Session overview](./index.md)
-- [Related packages](./related.md)
-- [Remix package index](../index.md)
diff --git a/docs/agents/remix/static-middleware.md b/docs/agents/remix/static-middleware/README.md
similarity index 64%
rename from docs/agents/remix/static-middleware.md
rename to docs/agents/remix/static-middleware/README.md
index c5f9ac6..4e90199 100644
--- a/docs/agents/remix/static-middleware.md
+++ b/docs/agents/remix/static-middleware/README.md
@@ -1,30 +1,23 @@
# static-middleware
-Source: https://github.com/remix-run/remix/tree/main/packages/static-middleware
-
-## README
-
-Middleware for serving static files from the filesystem for use with
-[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router).
-
-Serves static files from a directory with support for ETags, range requests, and
-conditional requests.
+Static file serving middleware for Remix. Serves static files from a directory
+with support for ETags, range requests, and conditional requests.
## Features
-- [ETag support](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
+- [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) support
(weak and strong)
-- [Range request support](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
- (HTTP 206 Partial Content)
-- [Conditional request support](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)
- (If-None-Match, If-Modified-Since)
+- [Range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
+ support (HTTP 206 Partial Content)
+- [Conditional request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)
+ support (If-None-Match, If-Modified-Since)
- Path traversal protection
-- Automatic fall through to next middleware/handler if file not found
+- Automatic fallback to next middleware/handler if file not found
## Installation
```sh
-bun add @remix-run/static-middleware
+npm i remix
```
## Usage
@@ -32,8 +25,8 @@ bun add @remix-run/static-middleware
Static middleware is useful for serving static files from a directory.
```ts
-import { createRouter } from '@remix-run/fetch-router'
-import { staticFiles } from '@remix-run/static-middleware'
+import { createRouter } from 'remix/fetch-router'
+import { staticFiles } from 'remix/static-middleware'
let router = createRouter({
middleware: [staticFiles('./public')],
@@ -103,7 +96,3 @@ let router = createRouter({
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/docs/agents/remix/tar-parser.md b/docs/agents/remix/tar-parser/README.md
similarity index 62%
rename from docs/agents/remix/tar-parser.md
rename to docs/agents/remix/tar-parser/README.md
index 9f0b68d..baf99a8 100644
--- a/docs/agents/remix/tar-parser.md
+++ b/docs/agents/remix/tar-parser/README.md
@@ -1,21 +1,8 @@
# tar-parser
-Source: https://github.com/remix-run/remix/tree/main/packages/tar-parser
-
-## README
-
-`tar-parser` is a fast, efficient parser for
-[tar archives]().
-
-Tar archives are ubiquitous in software development, used for distributing
-packages, backing up files, and transferring data. Most existing JavaScript tar
-parsers are either Node.js-specific or don't handle streaming efficiently,
-forcing you to buffer entire archives in memory. This makes them unsuitable for
-serverless environments or processing large archives.
-
-`tar-parser` can be used in any JavaScript environment (not just Node.js) and
-processes archives as streams, making it ideal for modern web development across
-all runtimes.
+Streaming [tar archive]() parsing
+for JavaScript. `tar-parser` handles POSIX/GNU/PAX archives incrementally so
+large tar files can be processed without buffering the full payload.
## Features
@@ -29,10 +16,8 @@ all runtimes.
## Installation
-Install from [npm](https://www.npmjs.com/):
-
```sh
-bun add @remix-run/tar-parser
+npm i remix
```
## Usage
@@ -40,7 +25,7 @@ bun add @remix-run/tar-parser
The main parser interface is the `parseTar(archive, handler)` function:
```ts
-import { parseTar } from '@remix-run/tar-parser'
+import { parseTar } from 'remix/tar-parser'
let response = await fetch(
'https://github.com/remix-run/remix/archive/refs/heads/main.tar.gz',
@@ -72,16 +57,19 @@ Node.js.
```
> @remix-run/tar-parser@0.0.0 bench /Users/michael/Projects/remix-the-web/packages/tar-parser
-> node --disable-warning=ExperimentalWarning ./bench/runner.ts
+> node ./bench/runner.ts
Platform: Darwin (24.0.0)
CPU: Apple M1 Pro
Date: 12/6/2024, 11:00:55 AM
Node.js v22.8.0
-(index) | lodash npm package
-tar-parser | '6.23 ms +/- 0.58'
-tar-stream | '6.72 ms +/- 2.24'
-node-tar | '6.49 ms +/- 0.44'
+┌────────────┬────────────────────┐
+│ (index) │ lodash npm package │
+├────────────┼────────────────────┤
+│ tar-parser │ '6.23 ms ± 0.58' │
+│ tar-stream │ '6.72 ms ± 2.24' │
+│ node-tar │ '6.49 ms ± 0.44' │
+└────────────┴────────────────────┘
```
## Related Packages
@@ -98,7 +86,3 @@ adopts the same core parsing algorithm, utility functions, and many test cases.
## License
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
-
-## Navigation
-
-- [Remix package index](./index.md)
diff --git a/mock-servers/ai/db-tables.ts b/mock-servers/ai/db-tables.ts
index 70ba010..68a4475 100644
--- a/mock-servers/ai/db-tables.ts
+++ b/mock-servers/ai/db-tables.ts
@@ -1,17 +1,16 @@
-import { createTable } from 'remix/data-table'
-import { number, string } from 'remix/data-schema'
+import { column, table } from 'remix/data-table'
-export const aiCapturedRequestsTable = createTable({
+export const aiCapturedRequestsTable = table({
name: 'ai_captured_requests',
columns: {
- id: string(),
- token_hash: string(),
- received_at: number(),
- scenario: string(),
- last_user_message: string(),
- tool_names_json: string(),
- request_json: string(),
- response_text: string(),
+ id: column.text(),
+ token_hash: column.text(),
+ received_at: column.integer(),
+ scenario: column.text(),
+ last_user_message: column.text(),
+ tool_names_json: column.text(),
+ request_json: column.text(),
+ response_text: column.text(),
},
primaryKey: 'id',
})
diff --git a/mock-servers/resend/db-tables.ts b/mock-servers/resend/db-tables.ts
index 2dc050d..60dfa5b 100644
--- a/mock-servers/resend/db-tables.ts
+++ b/mock-servers/resend/db-tables.ts
@@ -1,17 +1,16 @@
-import { createTable } from 'remix/data-table'
-import { number, string } from 'remix/data-schema'
+import { column, table } from 'remix/data-table'
-export const resendCapturedEmailsTable = createTable({
+export const resendCapturedEmailsTable = table({
name: 'resend_captured_emails',
columns: {
- id: string(),
- token_hash: string(),
- received_at: number(),
- from_email: string(),
- to_json: string(),
- subject: string(),
- html: string(),
- payload_json: string(),
+ id: column.text(),
+ token_hash: column.text(),
+ received_at: column.integer(),
+ from_email: column.text(),
+ to_json: column.text(),
+ subject: column.text(),
+ html: column.text(),
+ payload_json: column.text(),
},
primaryKey: 'id',
})
diff --git a/package.json b/package.json
index ff7d1dd..3cb2d18 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,7 @@
"@modelcontextprotocol/sdk": "1.26.0",
"agents": "^0.7.6",
"get-port": "^7.1.0",
- "remix": "3.0.0-alpha.3",
+ "remix": "3.0.0-alpha.4",
"workers-ai-provider": "^3.1.2",
"zod": "^4.3.6"
}
diff --git a/server/handlers/account.ts b/server/handlers/account.ts
index 6fc3e9b..907a307 100644
--- a/server/handlers/account.ts
+++ b/server/handlers/account.ts
@@ -7,7 +7,7 @@ import { type routes } from '#server/routes.ts'
export const account = {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (!session) {
diff --git a/server/handlers/auth-handler.test.ts b/server/handlers/auth-handler.test.ts
index 5bb7e7e..05c6481 100644
--- a/server/handlers/auth-handler.test.ts
+++ b/server/handlers/auth-handler.test.ts
@@ -20,7 +20,7 @@ function createAuthRequest(
const context = new RequestContext(request)
return {
- run: () => handler.action(context),
+ run: () => handler.handler(context),
}
}
diff --git a/server/handlers/auth-page.ts b/server/handlers/auth-page.ts
index 1266aa8..38c2a62 100644
--- a/server/handlers/auth-page.ts
+++ b/server/handlers/auth-page.ts
@@ -12,7 +12,7 @@ function normalizeRedirectTo(value: string | null) {
export function createAuthPageHandler() {
return {
middleware: [],
- async action({ request }: { request: Request }) {
+ async handler({ request }: { request: Request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (session) {
const url = new URL(request.url)
diff --git a/server/handlers/auth.ts b/server/handlers/auth.ts
index edde895..eaa7eba 100644
--- a/server/handlers/auth.ts
+++ b/server/handlers/auth.ts
@@ -31,7 +31,7 @@ export function createAuthHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request, url }) {
+ async handler({ request, url }) {
let body: unknown
try {
diff --git a/server/handlers/chat-threads.ts b/server/handlers/chat-threads.ts
index bcd1c21..4165352 100644
--- a/server/handlers/chat-threads.ts
+++ b/server/handlers/chat-threads.ts
@@ -20,7 +20,7 @@ export function createChatThreadsHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const user = await readAuthenticatedAppUser(request, appEnv as Env)
if (!user) {
return jsonResponse(
@@ -69,7 +69,7 @@ export function createDeleteChatThreadHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const user = await readAuthenticatedAppUser(request, appEnv as Env)
if (!user) {
return jsonResponse(
@@ -122,7 +122,7 @@ export function createUpdateChatThreadHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const user = await readAuthenticatedAppUser(request, appEnv as Env)
if (!user) {
return jsonResponse(
diff --git a/server/handlers/chat.ts b/server/handlers/chat.ts
index 9a67f5c..5e62aaf 100644
--- a/server/handlers/chat.ts
+++ b/server/handlers/chat.ts
@@ -7,7 +7,7 @@ import { type routes } from '#server/routes.ts'
export const chat = {
middleware: [],
- async action({ request }) {
+ async handler({ request }: { request: Request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (!session) {
diff --git a/server/handlers/health-handler.test.ts b/server/handlers/health-handler.test.ts
index aed5db6..9f60d40 100644
--- a/server/handlers/health-handler.test.ts
+++ b/server/handlers/health-handler.test.ts
@@ -10,7 +10,7 @@ function createHealthRequestContext() {
test('health handler returns ok with null commit SHA when unset', async () => {
const handler = createHealthHandler({ APP_COMMIT_SHA: undefined })
- const response = await handler.action(createHealthRequestContext())
+ const response = await handler.handler(createHealthRequestContext())
expect(response.status).toBe(200)
expect(response.headers.get('Cache-Control')).toBe('no-store')
@@ -23,7 +23,7 @@ test('health handler returns the configured commit SHA', async () => {
APP_COMMIT_SHA: 'f2d82dba4ba50cf2ad3f56f5c88f7b8ef5f97d8e',
})
- const response = await handler.action(createHealthRequestContext())
+ const response = await handler.handler(createHealthRequestContext())
expect(response.status).toBe(200)
expect(response.headers.get('X-App-Commit-Sha')).toBe(
diff --git a/server/handlers/health.ts b/server/handlers/health.ts
index e26f2f0..02cc3fc 100644
--- a/server/handlers/health.ts
+++ b/server/handlers/health.ts
@@ -9,7 +9,7 @@ type HealthEnv = {
export function createHealthHandler(appEnv: HealthEnv) {
return {
middleware: [],
- async action() {
+ async handler() {
const commitSha = appEnv.APP_COMMIT_SHA ?? null
return Response.json(
{ ok: true, commitSha },
diff --git a/server/handlers/home.ts b/server/handlers/home.ts
index 133fe29..d4aeddb 100644
--- a/server/handlers/home.ts
+++ b/server/handlers/home.ts
@@ -5,7 +5,7 @@ import { type routes } from '#server/routes.ts'
export const home = {
middleware: [],
- async action() {
+ async handler() {
return render(Layout({}))
},
} satisfies BuildAction
diff --git a/server/handlers/logout.ts b/server/handlers/logout.ts
index 3bd9be6..5716946 100644
--- a/server/handlers/logout.ts
+++ b/server/handlers/logout.ts
@@ -4,7 +4,7 @@ import { type routes } from '#server/routes.ts'
export const logout = {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const cookie = await destroyAuthCookie(isSecureRequest(request))
const location = new URL('/login', request.url)
diff --git a/server/handlers/password-reset.ts b/server/handlers/password-reset.ts
index f275e1f..e0ebe2d 100644
--- a/server/handlers/password-reset.ts
+++ b/server/handlers/password-reset.ts
@@ -73,7 +73,7 @@ export function createPasswordResetRequestHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request, url }) {
+ async handler({ request, url }) {
let body: unknown
try {
body = await request.json()
@@ -192,7 +192,7 @@ export function createPasswordResetConfirmHandler(appEnv: AppEnv) {
return {
middleware: [],
- async action({ request, url }) {
+ async handler({ request, url }) {
let body: unknown
try {
body = await request.json()
diff --git a/server/handlers/session-handler.test.ts b/server/handlers/session-handler.test.ts
index 3b8408f..e0d2309 100644
--- a/server/handlers/session-handler.test.ts
+++ b/server/handlers/session-handler.test.ts
@@ -48,7 +48,7 @@ test('session handler renews remembered sessions after two weeks', async () => {
)
const response = await withMockedNow(now, () =>
- session.action(createSessionRequestContext(cookie)),
+ session.handler(createSessionRequestContext(cookie)),
)
expect(response.status).toBe(200)
@@ -68,7 +68,7 @@ test('session handler keeps remembered sessions unchanged before renewal window'
)
const response = await withMockedNow(now, () =>
- session.action(createSessionRequestContext(cookie)),
+ session.handler(createSessionRequestContext(cookie)),
)
expect(response.status).toBe(200)
diff --git a/server/handlers/session.ts b/server/handlers/session.ts
index 34c47b3..2c04398 100644
--- a/server/handlers/session.ts
+++ b/server/handlers/session.ts
@@ -15,7 +15,7 @@ function jsonResponse(data: unknown, init?: ResponseInit) {
export const session = {
middleware: [],
- async action({ request }) {
+ async handler({ request }) {
const { session, setCookie } = await readAuthSessionResult(request)
if (!session) {
return jsonResponse({ ok: false })
diff --git a/server/router.ts b/server/router.ts
index 8c97c18..d6dde9b 100644
--- a/server/router.ts
+++ b/server/router.ts
@@ -30,29 +30,26 @@ export function createAppRouter(appEnv: AppEnv) {
},
})
const chatThreadsHandler = createChatThreadsHandler(appEnv)
-
- router.map(routes.home, home)
- router.map(routes.chat, chat)
- router.map(routes.chatThread, chat)
- router.map(routes.chatThreads, chatThreadsHandler)
- router.map(routes.chatThreadsCreate, chatThreadsHandler)
- router.map(routes.chatThreadsUpdate, createUpdateChatThreadHandler(appEnv))
- router.map(routes.chatThreadsDelete, createDeleteChatThreadHandler(appEnv))
- router.map(routes.health, createHealthHandler(appEnv))
- router.map(routes.login, login)
- router.map(routes.signup, signup)
- router.map(routes.account, account)
- router.map(routes.auth, createAuthHandler(appEnv))
- router.map(routes.session, session)
- router.map(routes.logout, logout)
- router.map(
- routes.passwordResetRequest,
- createPasswordResetRequestHandler(appEnv),
- )
- router.map(
- routes.passwordResetConfirm,
- createPasswordResetConfirmHandler(appEnv),
- )
+ router.map(routes, {
+ actions: {
+ home,
+ chat,
+ chatThread: chat,
+ chatThreads: chatThreadsHandler,
+ chatThreadsCreate: chatThreadsHandler,
+ chatThreadsUpdate: createUpdateChatThreadHandler(appEnv),
+ chatThreadsDelete: createDeleteChatThreadHandler(appEnv),
+ health: createHealthHandler(appEnv),
+ login,
+ signup,
+ account,
+ auth: createAuthHandler(appEnv),
+ session,
+ logout,
+ passwordResetRequest: createPasswordResetRequestHandler(appEnv),
+ passwordResetConfirm: createPasswordResetConfirmHandler(appEnv),
+ },
+ })
return router
}
diff --git a/types/tsconfig-client.json b/types/tsconfig-client.json
index b9518f9..b0e81dc 100644
--- a/types/tsconfig-client.json
+++ b/types/tsconfig-client.json
@@ -4,10 +4,16 @@
"composite": true,
"allowImportingTsExtensions": true,
"tsBuildInfoFile": "../node_modules/.tmp/tsconfig.client.tsbuildinfo",
+ "baseUrl": "..",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": [],
"jsx": "react-jsx",
"jsxImportSource": "remix/component"
},
- "include": ["../client/**/*.ts", "../client/**/*.tsx", "../shared/**/*.ts"]
+ "include": [
+ "../client/**/*.d.ts",
+ "../client/**/*.ts",
+ "../client/**/*.tsx",
+ "../shared/**/*.ts"
+ ]
}
diff --git a/worker/d1-data-table-adapter.ts b/worker/d1-data-table-adapter.ts
index 4b61be6..c1bab6a 100644
--- a/worker/d1-data-table-adapter.ts
+++ b/worker/d1-data-table-adapter.ts
@@ -1,23 +1,23 @@
import {
- getTableName,
getTablePrimaryKey,
type AdapterCapabilityOverrides,
- type AdapterExecuteRequest,
- type AdapterResult,
- type AdapterStatement,
+ type DataManipulationOperation,
+ type DataManipulationRequest,
+ type DataManipulationResult,
+ type DataMigrationOperation,
+ type DataMigrationRequest,
+ type DataMigrationResult,
type DatabaseAdapter,
+ type SqlStatement,
+ type TableRef,
type TransactionOptions,
type TransactionToken,
} from 'remix/data-table'
-
-type SqliteCompileContext = {
- values: Array
-}
-
-type CompiledSqlStatement = {
- text: string
- values: Array
-}
+import { isDataManipulationOperation } from 'remix/data-table/sql-helpers'
+import {
+ compileSqliteMigrationOperations,
+ compileSqliteOperation,
+} from '#worker/d1-sqlite-compiler.ts'
type D1Meta = {
changes?: number
@@ -52,6 +52,8 @@ export class D1DataTableAdapter implements DatabaseAdapter {
returning: boolean
savepoints: boolean
upsert: boolean
+ transactionalDdl: boolean
+ migrationLock: boolean
}
#database: D1Database
@@ -69,68 +71,129 @@ export class D1DataTableAdapter implements DatabaseAdapter {
returning: options?.capabilities?.returning ?? true,
savepoints: options?.capabilities?.savepoints ?? false,
upsert: options?.capabilities?.upsert ?? true,
+ transactionalDdl: options?.capabilities?.transactionalDdl ?? true,
+ migrationLock: options?.capabilities?.migrationLock ?? false,
+ }
+ }
+
+ compileSql(
+ operation: DataManipulationOperation | DataMigrationOperation,
+ ): Array {
+ if (isDataManipulationOperation(operation)) {
+ const compiled = compileSqliteOperation(operation)
+ return [{ text: compiled.text, values: compiled.values }]
}
+ return compileSqliteMigrationOperations(operation)
}
- async execute(request: AdapterExecuteRequest): Promise {
+ async execute(
+ request: DataManipulationRequest,
+ ): Promise {
if (
- request.statement.kind === 'insertMany' &&
- request.statement.values.length === 0
+ request.operation.kind === 'insertMany' &&
+ request.operation.values.length === 0
) {
return {
affectedRows: 0,
insertId: undefined,
- rows: request.statement.returning ? [] : undefined,
+ rows: request.operation.returning ? [] : undefined,
}
}
- const statement = compileSqliteStatement(request.statement)
+ const statement = this.compileSql(request.operation)[0]
+ if (!statement) {
+ throw new Error('Expected SQL statement for D1 execution')
+ }
const prepared = this.#database
.prepare(statement.text)
.bind(...statement.values) as unknown as D1PreparedQuery
const shouldReadRows =
- request.statement.kind === 'select' ||
- request.statement.kind === 'count' ||
- request.statement.kind === 'exists' ||
- hasReturningClause(request.statement)
+ request.operation.kind === 'select' ||
+ request.operation.kind === 'count' ||
+ request.operation.kind === 'exists' ||
+ hasReturningClause(request.operation)
if (shouldReadRows) {
const result = (await prepared.all()) as D1StatementResult
let rows = normalizeRows(result.results ?? [])
if (
- request.statement.kind === 'count' ||
- request.statement.kind === 'exists'
+ request.operation.kind === 'count' ||
+ request.operation.kind === 'exists'
) {
rows = normalizeCountRows(rows)
}
return {
rows,
affectedRows: normalizeAffectedRowsForReader(
- request.statement.kind,
+ request.operation.kind,
rows,
- result.meta,
),
insertId: normalizeInsertIdForReader(
- request.statement.kind,
- request.statement,
+ request.operation.kind,
+ request.operation,
rows,
- result.meta,
),
}
}
const result = (await prepared.run()) as D1StatementResult
return {
- affectedRows: normalizeAffectedRowsForRun(request.statement.kind, result),
+ affectedRows: normalizeAffectedRowsForRun(request.operation.kind, result),
insertId: normalizeInsertIdForRun(
- request.statement.kind,
- request.statement,
+ request.operation.kind,
+ request.operation,
result,
),
}
}
+ async migrate(request: DataMigrationRequest): Promise {
+ const statements = this.compileSql(request.operation)
+ for (const statement of statements) {
+ await this.#database.prepare(statement.text).bind(...statement.values).run()
+ }
+ return {
+ affectedOperations: statements.length,
+ }
+ }
+
+ async hasTable(
+ table: TableRef,
+ transaction?: TransactionToken,
+ ): Promise {
+ if (transaction) {
+ this.#assertTransaction(transaction)
+ }
+ const masterTable = table.schema
+ ? quoteIdentifier(table.schema) + '.sqlite_master'
+ : 'sqlite_master'
+ const result = (await this.#database
+ .prepare(
+ 'select 1 from ' + masterTable + ' where type = ? and name = ? limit 1',
+ )
+ .bind('table', table.name)
+ .all()) as D1StatementResult
+ return (result.results?.length ?? 0) > 0
+ }
+
+ async hasColumn(
+ table: TableRef,
+ column: string,
+ transaction?: TransactionToken,
+ ): Promise {
+ if (transaction) {
+ this.#assertTransaction(transaction)
+ }
+ const schemaPrefix = table.schema ? quoteIdentifier(table.schema) + '.' : ''
+ const result = (await this.#database
+ .prepare(
+ 'pragma ' + schemaPrefix + 'table_info(' + quoteIdentifier(table.name) + ')',
+ )
+ .all()) as D1StatementResult
+ return (result.results ?? []).some((row) => row.name === column)
+ }
+
async beginTransaction(
options?: TransactionOptions,
): Promise {
@@ -194,7 +257,7 @@ export function createD1DataTableAdapter(
return new D1DataTableAdapter(database, options)
}
-function hasReturningClause(statement: AdapterStatement) {
+function hasReturningClause(statement: DataManipulationRequest['operation']) {
return (
(statement.kind === 'insert' ||
statement.kind === 'insertMany' ||
@@ -237,24 +300,19 @@ function normalizeCountRows(rows: Array>) {
}
function normalizeAffectedRowsForReader(
- kind: AdapterStatement['kind'],
+ kind: DataManipulationRequest['operation']['kind'],
rows: Array>,
- meta?: D1Meta,
) {
if (isWriteStatementKind(kind)) {
- if (typeof meta?.changes === 'number') {
- return meta.changes
- }
return rows.length
}
return undefined
}
function normalizeInsertIdForReader(
- kind: AdapterStatement['kind'],
- statement: AdapterStatement,
+ kind: DataManipulationRequest['operation']['kind'],
+ statement: DataManipulationRequest['operation'],
rows: Array>,
- meta?: D1Meta,
) {
if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
return undefined
@@ -265,14 +323,14 @@ function normalizeInsertIdForReader(
}
const key = primaryKey[0]
if (!key) {
- return meta?.last_row_id
+ return undefined
}
const row = rows[rows.length - 1]
- return row?.[key] ?? meta?.last_row_id
+ return row?.[key]
}
function normalizeAffectedRowsForRun(
- kind: AdapterStatement['kind'],
+ kind: DataManipulationRequest['operation']['kind'],
result: D1StatementResult,
) {
if (kind === 'select' || kind === 'count' || kind === 'exists') {
@@ -282,8 +340,8 @@ function normalizeAffectedRowsForRun(
}
function normalizeInsertIdForRun(
- kind: AdapterStatement['kind'],
- statement: AdapterStatement,
+ kind: DataManipulationRequest['operation']['kind'],
+ statement: DataManipulationRequest['operation'],
result: D1StatementResult,
) {
if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
@@ -295,7 +353,7 @@ function normalizeInsertIdForRun(
return result.meta?.last_row_id
}
-function isWriteStatementKind(kind: AdapterStatement['kind']) {
+function isWriteStatementKind(kind: DataManipulationRequest['operation']['kind']) {
return (
kind === 'insert' ||
kind === 'insertMany' ||
@@ -305,14 +363,14 @@ function isWriteStatementKind(kind: AdapterStatement['kind']) {
)
}
-function isInsertStatementKind(kind: AdapterStatement['kind']) {
+function isInsertStatementKind(kind: DataManipulationRequest['operation']['kind']) {
return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'
}
function isInsertStatement(
- statement: AdapterStatement,
+ statement: DataManipulationRequest['operation'],
): statement is Extract<
- AdapterStatement,
+ DataManipulationRequest['operation'],
{ kind: 'insert' | 'insertMany' | 'upsert' }
> {
return (
@@ -322,572 +380,6 @@ function isInsertStatement(
)
}
-/**
- * Adapted from `@remix-run/data-table-sqlite` SQL compiler to keep this D1
- * adapter self-contained without depending on internal package paths.
- */
-function compileSqliteStatement(
- statement: AdapterStatement,
-): CompiledSqlStatement {
- if (statement.kind === 'raw') {
- return {
- text: statement.sql.text,
- values: [...statement.sql.values],
- }
- }
-
- const context: SqliteCompileContext = { values: [] }
-
- if (statement.kind === 'select') {
- let selection = '*'
- if (statement.select !== '*') {
- selection = statement.select
- .map(
- (field) =>
- quotePath(field.column) + ' as ' + quoteIdentifier(field.alias),
- )
- .join(', ')
- }
- return {
- text:
- 'select ' +
- (statement.distinct ? 'distinct ' : '') +
- selection +
- compileFromClause(
- statement.table,
- statement.joins as Array,
- context,
- ) +
- compileWhereClause(statement.where as Array, context) +
- compileGroupByClause(statement.groupBy) +
- compileHavingClause(statement.having as Array, context) +
- compileOrderByClause(statement.orderBy as Array) +
- compileLimitClause(statement.limit) +
- compileOffsetClause(statement.offset),
- values: context.values,
- }
- }
-
- if (statement.kind === 'count' || statement.kind === 'exists') {
- const inner =
- 'select 1' +
- compileFromClause(
- statement.table,
- statement.joins as Array,
- context,
- ) +
- compileWhereClause(statement.where as Array, context) +
- compileGroupByClause(statement.groupBy) +
- compileHavingClause(statement.having as Array, context)
- return {
- text:
- 'select count(*) as ' +
- quoteIdentifier('count') +
- ' from (' +
- inner +
- ') as ' +
- quoteIdentifier('__dt_count'),
- values: context.values,
- }
- }
-
- if (statement.kind === 'insert') {
- return compileInsertStatement(
- statement.table,
- statement.values as Record,
- statement.returning,
- context,
- )
- }
-
- if (statement.kind === 'insertMany') {
- return compileInsertManyStatement(
- statement.table,
- statement.values as Array>,
- statement.returning,
- context,
- )
- }
-
- if (statement.kind === 'update') {
- const columns = Object.keys(statement.changes)
- return {
- text:
- 'update ' +
- quotePath(getTableName(statement.table)) +
- ' set ' +
- columns
- .map(
- (column) =>
- quotePath(column) +
- ' = ' +
- pushValue(
- context,
- (statement.changes as Record)[column],
- ),
- )
- .join(', ') +
- compileWhereClause(statement.where as Array, context) +
- compileReturningClause(statement.returning),
- values: context.values,
- }
- }
-
- if (statement.kind === 'delete') {
- return {
- text:
- 'delete from ' +
- quotePath(getTableName(statement.table)) +
- compileWhereClause(statement.where as Array, context) +
- compileReturningClause(statement.returning),
- values: context.values,
- }
- }
-
- if (statement.kind === 'upsert') {
- return compileUpsertStatement(statement, context)
- }
-
- throw new Error('Unsupported statement kind')
-}
-
-function compileInsertStatement(
- table: Extract['table'],
- values: Record,
- returning: Extract['returning'],
- context: SqliteCompileContext,
-): CompiledSqlStatement {
- const columns = Object.keys(values)
- if (columns.length === 0) {
- return {
- text:
- 'insert into ' +
- quotePath(getTableName(table)) +
- ' default values' +
- compileReturningClause(returning),
- values: context.values,
- }
- }
-
- return {
- text:
- 'insert into ' +
- quotePath(getTableName(table)) +
- ' (' +
- columns.map((column) => quotePath(column)).join(', ') +
- ') values (' +
- columns.map((column) => pushValue(context, values[column])).join(', ') +
- ')' +
- compileReturningClause(returning),
- values: context.values,
- }
-}
-
-function compileInsertManyStatement(
- table: Extract['table'],
- rows: Array>,
- returning: Extract['returning'],
- context: SqliteCompileContext,
-): CompiledSqlStatement {
- if (rows.length === 0) {
- return {
- text: 'select 0 where 1 = 0',
- values: context.values,
- }
- }
-
- const columns = collectColumns(rows)
- if (columns.length === 0) {
- return {
- text:
- 'insert into ' +
- quotePath(getTableName(table)) +
- ' default values' +
- compileReturningClause(returning),
- values: context.values,
- }
- }
-
- return {
- text:
- 'insert into ' +
- quotePath(getTableName(table)) +
- ' (' +
- columns.map((column) => quotePath(column)).join(', ') +
- ') values ' +
- rows
- .map(
- (row) =>
- '(' +
- columns
- .map((column) => {
- const value = Object.prototype.hasOwnProperty.call(row, column)
- ? row[column]
- : null
- return pushValue(context, value)
- })
- .join(', ') +
- ')',
- )
- .join(', ') +
- compileReturningClause(returning),
- values: context.values,
- }
-}
-
-function compileUpsertStatement(
- statement: Extract,
- context: SqliteCompileContext,
-): CompiledSqlStatement {
- const insertColumns = Object.keys(statement.values)
- const conflictTarget = statement.conflictTarget ?? [
- ...getTablePrimaryKey(statement.table),
- ]
- if (insertColumns.length === 0) {
- throw new Error('upsert requires at least one value')
- }
-
- const updateValues = statement.update ?? statement.values
- const updateColumns = Object.keys(updateValues)
- let conflictClause = ''
-
- if (updateColumns.length === 0) {
- conflictClause =
- ' on conflict (' +
- conflictTarget.map((column) => quotePath(column)).join(', ') +
- ') do nothing'
- } else {
- conflictClause =
- ' on conflict (' +
- conflictTarget.map((column) => quotePath(column)).join(', ') +
- ') do update set ' +
- updateColumns
- .map(
- (column) =>
- quotePath(column) +
- ' = ' +
- pushValue(
- context,
- (updateValues as Record)[column],
- ),
- )
- .join(', ')
- }
-
- return {
- text:
- 'insert into ' +
- quotePath(getTableName(statement.table)) +
- ' (' +
- insertColumns.map((column) => quotePath(column)).join(', ') +
- ') values (' +
- insertColumns
- .map((column) =>
- pushValue(
- context,
- (statement.values as Record)[column],
- ),
- )
- .join(', ') +
- ')' +
- conflictClause +
- compileReturningClause(statement.returning),
- values: context.values,
- }
-}
-
-function compileFromClause(
- table: AdapterStatement extends infer T
- ? T extends { table: infer tableType }
- ? tableType
- : never
- : never,
- joins: Array,
- context: SqliteCompileContext,
-) {
- let output = ' from ' + quotePath(getTableName(table))
- for (const join of joins) {
- const typedJoin = join as {
- type: 'inner' | 'left' | 'right'
- table: Parameters[0]
- on: unknown
- }
- output +=
- ' ' +
- normalizeJoinType(typedJoin.type) +
- ' join ' +
- quotePath(getTableName(typedJoin.table)) +
- ' on ' +
- compilePredicate(typedJoin.on, context)
- }
- return output
-}
-
-function compileWhereClause(
- predicates: Array,
- context: SqliteCompileContext,
-) {
- if (predicates.length === 0) {
- return ''
- }
- return (
- ' where ' +
- predicates
- .map((predicate) => '(' + compilePredicate(predicate, context) + ')')
- .join(' and ')
- )
-}
-
-function compileGroupByClause(columns: Array) {
- if (columns.length === 0) {
- return ''
- }
- return ' group by ' + columns.map((column) => quotePath(column)).join(', ')
-}
-
-function compileHavingClause(
- predicates: Array,
- context: SqliteCompileContext,
-) {
- if (predicates.length === 0) {
- return ''
- }
- return (
- ' having ' +
- predicates
- .map((predicate) => '(' + compilePredicate(predicate, context) + ')')
- .join(' and ')
- )
-}
-
-function compileOrderByClause(orderBy: Array) {
- if (orderBy.length === 0) {
- return ''
- }
- return (
- ' order by ' +
- orderBy
- .map((clause) => {
- const typedClause = clause as {
- column: string
- direction: 'asc' | 'desc'
- }
- return (
- quotePath(typedClause.column) +
- ' ' +
- typedClause.direction.toUpperCase()
- )
- })
- .join(', ')
- )
-}
-
-function compileLimitClause(limit?: number) {
- if (limit === undefined) {
- return ''
- }
- return ' limit ' + String(limit)
-}
-
-function compileOffsetClause(offset?: number) {
- if (offset === undefined) {
- return ''
- }
- return ' offset ' + String(offset)
-}
-
-function compileReturningClause(returning?: '*' | Array) {
- if (!returning) {
- return ''
- }
- if (returning === '*') {
- return ' returning *'
- }
- return ' returning ' + returning.map((column) => quotePath(column)).join(', ')
-}
-
-function compilePredicate(
- predicate: unknown,
- context: SqliteCompileContext,
-): string {
- const typedPredicate = predicate as {
- type: string
- [column: string]: unknown
- }
-
- if (typedPredicate.type === 'comparison') {
- const column = quotePath(String(typedPredicate.column))
-
- if (typedPredicate.operator === 'eq') {
- if (
- typedPredicate.valueType === 'value' &&
- (typedPredicate.value === null || typedPredicate.value === undefined)
- ) {
- return column + ' is null'
- }
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return column + ' = ' + comparisonValue
- }
-
- if (typedPredicate.operator === 'ne') {
- if (
- typedPredicate.valueType === 'value' &&
- (typedPredicate.value === null || typedPredicate.value === undefined)
- ) {
- return column + ' is not null'
- }
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return column + ' <> ' + comparisonValue
- }
-
- if (typedPredicate.operator === 'gt') {
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return column + ' > ' + comparisonValue
- }
-
- if (typedPredicate.operator === 'gte') {
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return column + ' >= ' + comparisonValue
- }
-
- if (typedPredicate.operator === 'lt') {
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return column + ' < ' + comparisonValue
- }
-
- if (typedPredicate.operator === 'lte') {
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return column + ' <= ' + comparisonValue
- }
-
- if (
- typedPredicate.operator === 'in' ||
- typedPredicate.operator === 'notIn'
- ) {
- const values = Array.isArray(typedPredicate.value)
- ? typedPredicate.value
- : []
- if (values.length === 0) {
- return typedPredicate.operator === 'in' ? '1 = 0' : '1 = 1'
- }
-
- const keyword = typedPredicate.operator === 'in' ? 'in' : 'not in'
- return (
- column +
- ' ' +
- keyword +
- ' (' +
- values.map((value) => pushValue(context, value)).join(', ') +
- ')'
- )
- }
-
- if (typedPredicate.operator === 'like') {
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return column + ' like ' + comparisonValue
- }
-
- if (typedPredicate.operator === 'ilike') {
- const comparisonValue = compileComparisonValue(typedPredicate, context)
- return 'lower(' + column + ') like lower(' + comparisonValue + ')'
- }
- }
-
- if (typedPredicate.type === 'between') {
- return (
- quotePath(String(typedPredicate.column)) +
- ' between ' +
- pushValue(context, typedPredicate.lower) +
- ' and ' +
- pushValue(context, typedPredicate.upper)
- )
- }
-
- if (typedPredicate.type === 'null') {
- return (
- quotePath(String(typedPredicate.column)) +
- (typedPredicate.operator === 'isNull' ? ' is null' : ' is not null')
- )
- }
-
- if (typedPredicate.type === 'logical') {
- const predicates = Array.isArray(typedPredicate.predicates)
- ? typedPredicate.predicates
- : []
- if (predicates.length === 0) {
- return typedPredicate.operator === 'and' ? '1 = 1' : '1 = 0'
- }
- const joiner = typedPredicate.operator === 'and' ? ' and ' : ' or '
- return predicates
- .map((child) => '(' + compilePredicate(child, context) + ')')
- .join(joiner)
- }
-
- throw new Error('Unsupported predicate')
-}
-
-function compileComparisonValue(predicate: any, context: SqliteCompileContext) {
- if (predicate.valueType === 'column') {
- return quotePath(String(predicate.value))
- }
- return pushValue(context, predicate.value)
-}
-
-function normalizeJoinType(type: 'inner' | 'left' | 'right') {
- if (type === 'left') {
- return 'left'
- }
- if (type === 'right') {
- return 'right'
- }
- return 'inner'
-}
-
function quoteIdentifier(value: string) {
return '"' + value.replace(/"/g, '""') + '"'
}
-
-function quotePath(path: string) {
- if (path === '*') {
- return '*'
- }
- return path
- .split('.')
- .map((segment) => {
- if (segment === '*') {
- return '*'
- }
- return quoteIdentifier(segment)
- })
- .join('.')
-}
-
-function pushValue(context: SqliteCompileContext, value: unknown) {
- context.values.push(normalizeBoundValue(value))
- return '?'
-}
-
-function normalizeBoundValue(value: unknown) {
- if (typeof value === 'boolean') {
- return value ? 1 : 0
- }
- return value
-}
-
-function collectColumns(rows: Array>) {
- const columns: Array = []
- const seen = new Set()
- for (const row of rows) {
- for (const key in row) {
- if (!Object.prototype.hasOwnProperty.call(row, key)) {
- continue
- }
- if (seen.has(key)) {
- continue
- }
- seen.add(key)
- columns.push(key)
- }
- }
- return columns
-}
diff --git a/worker/d1-sqlite-compiler.ts b/worker/d1-sqlite-compiler.ts
new file mode 100644
index 0000000..281ce0d
--- /dev/null
+++ b/worker/d1-sqlite-compiler.ts
@@ -0,0 +1,938 @@
+import {
+ getTableName,
+ getTablePrimaryKey,
+ type ColumnDefinition,
+ type DataManipulationOperation,
+ type DataMigrationOperation,
+ type Predicate,
+ type SqlStatement,
+ type TableRef,
+} from 'remix/data-table'
+import {
+ quoteLiteral as quoteLiteralHelper,
+ quoteTableRef as quoteTableRefHelper,
+ collectColumns as collectColumnsHelper,
+ normalizeJoinType as normalizeJoinTypeHelper,
+ quotePath as quotePathHelper,
+} from 'remix/data-table/sql-helpers'
+
+type JoinClause = Extract['joins'][number]
+type UpsertOperation = Extract
+type OperationTable = Extract['table']
+
+type CompileContext = {
+ values: Array
+}
+
+export function compileSqliteOperation(
+ operation: DataManipulationOperation,
+): SqlStatement {
+ if (operation.kind === 'raw') {
+ return {
+ text: operation.sql.text,
+ values: [...operation.sql.values],
+ }
+ }
+
+ const context: CompileContext = { values: [] }
+
+ if (operation.kind === 'select') {
+ let selection = '*'
+
+ if (operation.select !== '*') {
+ selection = operation.select
+ .map((field) => quotePath(field.column) + ' as ' + quoteIdentifier(field.alias))
+ .join(', ')
+ }
+
+ return {
+ text:
+ 'select ' +
+ (operation.distinct ? 'distinct ' : '') +
+ selection +
+ compileFromClause(operation.table, operation.joins, context) +
+ compileWhereClause(operation.where, context) +
+ compileGroupByClause(operation.groupBy) +
+ compileHavingClause(operation.having, context) +
+ compileOrderByClause(operation.orderBy) +
+ compileLimitClause(operation.limit) +
+ compileOffsetClause(operation.offset),
+ values: context.values,
+ }
+ }
+
+ if (operation.kind === 'count' || operation.kind === 'exists') {
+ const inner =
+ 'select 1' +
+ compileFromClause(operation.table, operation.joins, context) +
+ compileWhereClause(operation.where, context) +
+ compileGroupByClause(operation.groupBy) +
+ compileHavingClause(operation.having, context)
+
+ return {
+ text:
+ 'select count(*) as ' +
+ quoteIdentifier('count') +
+ ' from (' +
+ inner +
+ ') as ' +
+ quoteIdentifier('__dt_count'),
+ values: context.values,
+ }
+ }
+
+ if (operation.kind === 'insert') {
+ return compileInsertOperation(
+ operation.table,
+ operation.values,
+ operation.returning,
+ context,
+ )
+ }
+
+ if (operation.kind === 'insertMany') {
+ return compileInsertManyOperation(
+ operation.table,
+ operation.values,
+ operation.returning,
+ context,
+ )
+ }
+
+ if (operation.kind === 'update') {
+ const columns = Object.keys(operation.changes)
+
+ return {
+ text:
+ 'update ' +
+ quotePath(getTableName(operation.table)) +
+ ' set ' +
+ columns
+ .map(
+ (column) =>
+ quotePath(column) +
+ ' = ' +
+ pushValue(context, operation.changes[column]),
+ )
+ .join(', ') +
+ compileWhereClause(operation.where, context) +
+ compileReturningClause(operation.returning),
+ values: context.values,
+ }
+ }
+
+ if (operation.kind === 'delete') {
+ return {
+ text:
+ 'delete from ' +
+ quotePath(getTableName(operation.table)) +
+ compileWhereClause(operation.where, context) +
+ compileReturningClause(operation.returning),
+ values: context.values,
+ }
+ }
+
+ if (operation.kind === 'upsert') {
+ return compileUpsertOperation(operation, context)
+ }
+
+ throw new Error('Unsupported operation kind')
+}
+
+export function compileSqliteMigrationOperations(
+ operation: DataMigrationOperation,
+): Array {
+ if (operation.kind === 'raw') {
+ return [{ text: operation.sql.text, values: [...operation.sql.values] }]
+ }
+
+ if (operation.kind === 'createTable') {
+ const columns = Object.keys(operation.columns).map(
+ (columnName) => {
+ const definition = operation.columns[columnName]
+ if (!definition) {
+ throw new Error('Missing sqlite column definition for ' + columnName)
+ }
+ return quoteIdentifier(columnName) + ' ' + compileSqliteColumn(definition)
+ },
+ )
+ const constraints: Array = []
+
+ if (operation.primaryKey) {
+ constraints.push(
+ 'constraint ' +
+ quoteIdentifier(operation.primaryKey.name) +
+ ' primary key (' +
+ operation.primaryKey.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')',
+ )
+ }
+
+ for (const unique of operation.uniques ?? []) {
+ constraints.push(
+ 'constraint ' +
+ quoteIdentifier(unique.name) +
+ ' unique (' +
+ unique.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')',
+ )
+ }
+
+ for (const check of operation.checks ?? []) {
+ constraints.push(
+ 'constraint ' + quoteIdentifier(check.name) + ' check (' + check.expression + ')',
+ )
+ }
+
+ for (const foreignKey of operation.foreignKeys ?? []) {
+ let clause =
+ 'constraint ' +
+ quoteIdentifier(foreignKey.name) +
+ ' foreign key (' +
+ foreignKey.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ') references ' +
+ quoteTableRef(foreignKey.references.table) +
+ ' (' +
+ foreignKey.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')'
+
+ if (foreignKey.onDelete) {
+ clause += ' on delete ' + foreignKey.onDelete
+ }
+
+ if (foreignKey.onUpdate) {
+ clause += ' on update ' + foreignKey.onUpdate
+ }
+
+ constraints.push(clause)
+ }
+
+ return [
+ {
+ text:
+ 'create table ' +
+ (operation.ifNotExists ? 'if not exists ' : '') +
+ quoteTableRef(operation.table) +
+ ' (' +
+ [...columns, ...constraints].join(', ') +
+ ')',
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'alterTable') {
+ const statements: Array = []
+
+ for (const change of operation.changes) {
+ let sql = 'alter table ' + quoteTableRef(operation.table) + ' '
+
+ if (change.kind === 'addColumn') {
+ sql +=
+ 'add column ' +
+ quoteIdentifier(change.column) +
+ ' ' +
+ compileSqliteColumn(change.definition)
+ } else if (change.kind === 'changeColumn') {
+ sql +=
+ 'alter column ' +
+ quoteIdentifier(change.column) +
+ ' type ' +
+ compileSqliteColumnType(change.definition)
+ } else if (change.kind === 'renameColumn') {
+ sql += 'rename column ' + quoteIdentifier(change.from) + ' to ' + quoteIdentifier(change.to)
+ } else if (change.kind === 'dropColumn') {
+ sql += 'drop column ' + quoteIdentifier(change.column)
+ } else if (change.kind === 'addPrimaryKey') {
+ sql +=
+ 'add primary key (' +
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')'
+ } else if (change.kind === 'dropPrimaryKey') {
+ sql += 'drop primary key'
+ } else if (change.kind === 'addUnique') {
+ sql +=
+ 'add constraint ' +
+ quoteIdentifier(change.constraint.name) +
+ ' unique (' +
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')'
+ } else if (change.kind === 'dropUnique') {
+ sql += 'drop constraint ' + quoteIdentifier(change.name)
+ } else if (change.kind === 'addForeignKey') {
+ sql +=
+ 'add constraint ' +
+ quoteIdentifier(change.constraint.name) +
+ ' foreign key (' +
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ') references ' +
+ quoteTableRef(change.constraint.references.table) +
+ ' (' +
+ change.constraint.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')'
+ } else if (change.kind === 'dropForeignKey') {
+ sql += 'drop constraint ' + quoteIdentifier(change.name)
+ } else if (change.kind === 'addCheck') {
+ sql +=
+ 'add constraint ' +
+ quoteIdentifier(change.constraint.name) +
+ ' check (' +
+ change.constraint.expression +
+ ')'
+ } else if (change.kind === 'dropCheck') {
+ sql += 'drop constraint ' + quoteIdentifier(change.name)
+ } else if (change.kind === 'setTableComment') {
+ continue
+ } else {
+ continue
+ }
+
+ statements.push({ text: sql, values: [] })
+ }
+
+ return statements
+ }
+
+ if (operation.kind === 'renameTable') {
+ return [
+ {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.from) +
+ ' rename to ' +
+ quoteIdentifier(operation.to.name),
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'dropTable') {
+ return [
+ {
+ text:
+ 'drop table ' +
+ (operation.ifExists ? 'if exists ' : '') +
+ quoteTableRef(operation.table),
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'createIndex') {
+ return [
+ {
+ text:
+ 'create ' +
+ (operation.index.unique ? 'unique ' : '') +
+ 'index ' +
+ (operation.ifNotExists ? 'if not exists ' : '') +
+ quoteIdentifier(operation.index.name) +
+ ' on ' +
+ quoteTableRef(operation.index.table) +
+ ' (' +
+ operation.index.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')' +
+ (operation.index.where ? ' where ' + operation.index.where : ''),
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'dropIndex') {
+ return [
+ {
+ text:
+ 'drop index ' +
+ (operation.ifExists ? 'if exists ' : '') +
+ quoteIdentifier(operation.name),
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'renameIndex') {
+ return [
+ {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.table) +
+ ' rename index ' +
+ quoteIdentifier(operation.from) +
+ ' to ' +
+ quoteIdentifier(operation.to),
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'addForeignKey') {
+ return [
+ {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.table) +
+ ' add constraint ' +
+ quoteIdentifier(operation.constraint.name) +
+ ' foreign key (' +
+ operation.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ') references ' +
+ quoteTableRef(operation.constraint.references.table) +
+ ' (' +
+ operation.constraint.references.columns
+ .map((column) => quoteIdentifier(column))
+ .join(', ') +
+ ')' +
+ (operation.constraint.onDelete ? ' on delete ' + operation.constraint.onDelete : '') +
+ (operation.constraint.onUpdate ? ' on update ' + operation.constraint.onUpdate : ''),
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'dropForeignKey') {
+ return [
+ {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.table) +
+ ' drop constraint ' +
+ quoteIdentifier(operation.name),
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'addCheck') {
+ return [
+ {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.table) +
+ ' add constraint ' +
+ quoteIdentifier(operation.constraint.name) +
+ ' check (' +
+ operation.constraint.expression +
+ ')',
+ values: [],
+ },
+ ]
+ }
+
+ if (operation.kind === 'dropCheck') {
+ return [
+ {
+ text:
+ 'alter table ' +
+ quoteTableRef(operation.table) +
+ ' drop constraint ' +
+ quoteIdentifier(operation.name),
+ values: [],
+ },
+ ]
+ }
+
+ throw new Error('Unsupported data migration operation kind')
+}
+
+function compileInsertOperation(
+ table: OperationTable,
+ values: Record,
+ returning: '*' | Array | undefined,
+ context: CompileContext,
+): SqlStatement {
+ const columns = Object.keys(values)
+
+ if (columns.length === 0) {
+ return {
+ text:
+ 'insert into ' +
+ quotePath(getTableName(table)) +
+ ' default values' +
+ compileReturningClause(returning),
+ values: context.values,
+ }
+ }
+
+ return {
+ text:
+ 'insert into ' +
+ quotePath(getTableName(table)) +
+ ' (' +
+ columns.map((column) => quotePath(column)).join(', ') +
+ ') values (' +
+ columns.map((column) => pushValue(context, values[column])).join(', ') +
+ ')' +
+ compileReturningClause(returning),
+ values: context.values,
+ }
+}
+
+function compileInsertManyOperation(
+ table: OperationTable,
+ rows: Array>,
+ returning: '*' | Array | undefined,
+ context: CompileContext,
+): SqlStatement {
+ if (rows.length === 0) {
+ return {
+ text: 'select 0 where 1 = 0',
+ values: context.values,
+ }
+ }
+
+ const columns = collectColumns(rows)
+
+ if (columns.length === 0) {
+ return {
+ text:
+ 'insert into ' +
+ quotePath(getTableName(table)) +
+ ' default values' +
+ compileReturningClause(returning),
+ values: context.values,
+ }
+ }
+
+ return {
+ text:
+ 'insert into ' +
+ quotePath(getTableName(table)) +
+ ' (' +
+ columns.map((column) => quotePath(column)).join(', ') +
+ ') values ' +
+ rows
+ .map(
+ (row) =>
+ '(' +
+ columns
+ .map((column) => {
+ const value = Object.prototype.hasOwnProperty.call(row, column)
+ ? row[column]
+ : null
+ return pushValue(context, value)
+ })
+ .join(', ') +
+ ')',
+ )
+ .join(', ') +
+ compileReturningClause(returning),
+ values: context.values,
+ }
+}
+
+function compileUpsertOperation(
+ operation: UpsertOperation,
+ context: CompileContext,
+): SqlStatement {
+ const insertColumns = Object.keys(operation.values)
+ const conflictTarget = operation.conflictTarget ?? [...getTablePrimaryKey(operation.table)]
+
+ if (insertColumns.length === 0) {
+ throw new Error('upsert requires at least one value')
+ }
+
+ const updateValues = operation.update ?? operation.values
+ const updateColumns = Object.keys(updateValues)
+
+ let conflictClause = ''
+
+ if (updateColumns.length === 0) {
+ conflictClause =
+ ' on conflict (' +
+ conflictTarget.map((column: string) => quotePath(column)).join(', ') +
+ ') do nothing'
+ } else {
+ conflictClause =
+ ' on conflict (' +
+ conflictTarget.map((column: string) => quotePath(column)).join(', ') +
+ ') do update set ' +
+ updateColumns
+ .map(
+ (column) =>
+ quotePath(column) + ' = ' + pushValue(context, updateValues[column]),
+ )
+ .join(', ')
+ }
+
+ return {
+ text:
+ 'insert into ' +
+ quotePath(getTableName(operation.table)) +
+ ' (' +
+ insertColumns.map((column) => quotePath(column)).join(', ') +
+ ') values (' +
+ insertColumns
+ .map((column) => pushValue(context, operation.values[column]))
+ .join(', ') +
+ ')' +
+ conflictClause +
+ compileReturningClause(operation.returning),
+ values: context.values,
+ }
+}
+
+function compileFromClause(
+ table: OperationTable,
+ joins: Array,
+ context: CompileContext,
+) {
+ let output = ' from ' + quotePath(getTableName(table))
+
+ for (const join of joins) {
+ output +=
+ ' ' +
+ normalizeJoinType(join.type) +
+ ' join ' +
+ quotePath(getTableName(join.table)) +
+ ' on ' +
+ compilePredicate(join.on, context)
+ }
+
+ return output
+}
+
+function compileWhereClause(
+ predicates: Array,
+ context: CompileContext,
+) {
+ if (predicates.length === 0) {
+ return ''
+ }
+
+ return (
+ ' where ' +
+ predicates
+ .map((predicate) => '(' + compilePredicate(predicate, context) + ')')
+ .join(' and ')
+ )
+}
+
+function compileGroupByClause(columns: Array) {
+ if (columns.length === 0) {
+ return ''
+ }
+
+ return ' group by ' + columns.map((column) => quotePath(column)).join(', ')
+}
+
+function compileHavingClause(
+ predicates: Array,
+ context: CompileContext,
+) {
+ if (predicates.length === 0) {
+ return ''
+ }
+
+ return (
+ ' having ' +
+ predicates
+ .map((predicate) => '(' + compilePredicate(predicate, context) + ')')
+ .join(' and ')
+ )
+}
+
+function compileOrderByClause(orderBy: Array<{ column: string; direction: 'asc' | 'desc' }>) {
+ if (orderBy.length === 0) {
+ return ''
+ }
+
+ return (
+ ' order by ' +
+ orderBy
+ .map((clause) => quotePath(clause.column) + ' ' + clause.direction.toUpperCase())
+ .join(', ')
+ )
+}
+
+function compileLimitClause(limit?: number) {
+ if (limit === undefined) {
+ return ''
+ }
+
+ return ' limit ' + String(limit)
+}
+
+function compileOffsetClause(offset?: number) {
+ if (offset === undefined) {
+ return ''
+ }
+
+ return ' offset ' + String(offset)
+}
+
+function compileReturningClause(returning?: '*' | Array) {
+ if (!returning) {
+ return ''
+ }
+
+ if (returning === '*') {
+ return ' returning *'
+ }
+
+ return ' returning ' + returning.map((column) => quotePath(column)).join(', ')
+}
+
+function compilePredicate(
+ predicate: unknown,
+ context: CompileContext,
+): string {
+ const typedPredicate = predicate as {
+ type: string
+ [column: string]: unknown
+ }
+
+ if (typedPredicate.type === 'comparison') {
+ const column = quotePath(String(typedPredicate.column))
+
+ if (typedPredicate.operator === 'eq') {
+ if (
+ typedPredicate.valueType === 'value' &&
+ (typedPredicate.value === null || typedPredicate.value === undefined)
+ ) {
+ return column + ' is null'
+ }
+
+ return column + ' = ' + compileComparisonValue(typedPredicate, context)
+ }
+
+ if (typedPredicate.operator === 'ne') {
+ if (
+ typedPredicate.valueType === 'value' &&
+ (typedPredicate.value === null || typedPredicate.value === undefined)
+ ) {
+ return column + ' is not null'
+ }
+
+ return column + ' <> ' + compileComparisonValue(typedPredicate, context)
+ }
+
+ if (typedPredicate.operator === 'gt') {
+ return column + ' > ' + compileComparisonValue(typedPredicate, context)
+ }
+
+ if (typedPredicate.operator === 'gte') {
+ return column + ' >= ' + compileComparisonValue(typedPredicate, context)
+ }
+
+ if (typedPredicate.operator === 'lt') {
+ return column + ' < ' + compileComparisonValue(typedPredicate, context)
+ }
+
+ if (typedPredicate.operator === 'lte') {
+ return column + ' <= ' + compileComparisonValue(typedPredicate, context)
+ }
+
+ if (
+ typedPredicate.operator === 'in' ||
+ typedPredicate.operator === 'notIn'
+ ) {
+ const values = Array.isArray(typedPredicate.value)
+ ? typedPredicate.value
+ : []
+
+ if (values.length === 0) {
+ return typedPredicate.operator === 'in' ? '1 = 0' : '1 = 1'
+ }
+
+ const keyword = typedPredicate.operator === 'in' ? 'in' : 'not in'
+
+ return (
+ column +
+ ' ' +
+ keyword +
+ ' (' +
+ values.map((value) => pushValue(context, value)).join(', ') +
+ ')'
+ )
+ }
+
+ if (typedPredicate.operator === 'like') {
+ return column + ' like ' + compileComparisonValue(typedPredicate, context)
+ }
+
+ if (typedPredicate.operator === 'ilike') {
+ return (
+ 'lower(' +
+ column +
+ ') like lower(' +
+ compileComparisonValue(typedPredicate, context) +
+ ')'
+ )
+ }
+ }
+
+ if (typedPredicate.type === 'between') {
+ return (
+ quotePath(String(typedPredicate.column)) +
+ ' between ' +
+ pushValue(context, typedPredicate.lower) +
+ ' and ' +
+ pushValue(context, typedPredicate.upper)
+ )
+ }
+
+ if (typedPredicate.type === 'null') {
+ return (
+ quotePath(String(typedPredicate.column)) +
+ (typedPredicate.operator === 'isNull' ? ' is null' : ' is not null')
+ )
+ }
+
+ if (typedPredicate.type === 'logical') {
+ const predicates = Array.isArray(typedPredicate.predicates)
+ ? typedPredicate.predicates
+ : []
+
+ if (predicates.length === 0) {
+ return typedPredicate.operator === 'and' ? '1 = 1' : '1 = 0'
+ }
+
+ const joiner = typedPredicate.operator === 'and' ? ' and ' : ' or '
+
+ return predicates
+ .map((child) => '(' + compilePredicate(child, context) + ')')
+ .join(joiner)
+ }
+
+ throw new Error('Unsupported predicate')
+}
+
+function compileComparisonValue(
+ predicate: {
+ type?: string
+ valueType?: unknown
+ value?: unknown
+ [key: string]: unknown
+ },
+ context: CompileContext,
+) {
+ if (predicate.valueType === 'column') {
+ return quotePath(String(predicate.value))
+ }
+
+ return pushValue(context, predicate.value)
+}
+
+function quoteLiteral(value: unknown) {
+ return quoteLiteralHelper(value, { booleansAsIntegers: true })
+}
+
+function quoteTableRef(table: TableRef) {
+ return quoteTableRefHelper(table, quoteIdentifier)
+}
+
+function compileSqliteColumn(definition: ColumnDefinition): string {
+ const parts = [compileSqliteColumnType(definition)]
+
+ if (definition.nullable === false) {
+ parts.push('not null')
+ }
+
+ if (definition.default) {
+ if (definition.default.kind === 'now') {
+ parts.push('default current_timestamp')
+ } else if (definition.default.kind === 'sql') {
+ parts.push('default ' + definition.default.expression)
+ } else {
+ parts.push('default ' + quoteLiteral(definition.default.value))
+ }
+ }
+
+ if (definition.primaryKey) {
+ parts.push('primary key')
+ }
+
+ if (definition.unique) {
+ parts.push('unique')
+ }
+
+ if (definition.computed) {
+ parts.push('generated always as (' + definition.computed.expression + ')')
+ parts.push(definition.computed.stored ? 'stored' : 'virtual')
+ }
+
+ if (definition.references) {
+ let clause =
+ 'references ' +
+ quoteTableRef(definition.references.table) +
+ ' (' +
+ definition.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
+ ')'
+
+ if (definition.references.onDelete) {
+ clause += ' on delete ' + definition.references.onDelete
+ }
+
+ if (definition.references.onUpdate) {
+ clause += ' on update ' + definition.references.onUpdate
+ }
+
+ parts.push(clause)
+ }
+
+ if (definition.checks && definition.checks.length > 0) {
+ for (const check of definition.checks) {
+ parts.push('check (' + check.expression + ')')
+ }
+ }
+
+ return parts.join(' ')
+}
+
+function compileSqliteColumnType(definition: ColumnDefinition): string {
+ if (definition.type === 'varchar' || definition.type === 'text') {
+ return 'text'
+ }
+ if (definition.type === 'integer' || definition.type === 'bigint') {
+ return 'integer'
+ }
+ if (definition.type === 'decimal') {
+ return 'numeric'
+ }
+ if (definition.type === 'boolean') {
+ return 'integer'
+ }
+ if (
+ definition.type === 'uuid' ||
+ definition.type === 'date' ||
+ definition.type === 'time' ||
+ definition.type === 'timestamp' ||
+ definition.type === 'json' ||
+ definition.type === 'enum'
+ ) {
+ return 'text'
+ }
+ if (definition.type === 'binary') {
+ return 'blob'
+ }
+ return 'text'
+}
+
+function quoteIdentifier(value: string) {
+ return '"' + value.replace(/"/g, '""') + '"'
+}
+
+function quotePath(path: string) {
+ return quotePathHelper(path, quoteIdentifier)
+}
+
+function normalizeJoinType(type: string) {
+ return normalizeJoinTypeHelper(type)
+}
+
+function pushValue(context: CompileContext, value: unknown) {
+ context.values.push(normalizeBoundValue(value))
+ return '?'
+}
+
+function normalizeBoundValue(value: unknown) {
+ if (typeof value === 'boolean') {
+ return value ? 1 : 0
+ }
+
+ return value
+}
+
+function collectColumns(rows: Array>) {
+ return collectColumnsHelper(rows)
+}
diff --git a/worker/db.ts b/worker/db.ts
index 346353c..702914a 100644
--- a/worker/db.ts
+++ b/worker/db.ts
@@ -1,43 +1,42 @@
-import { createDatabase, createTable, sql } from 'remix/data-table'
-import { nullable, number, optional, string } from 'remix/data-schema'
+import { column, createDatabase, sql, table } from 'remix/data-table'
import { createD1DataTableAdapter } from './d1-data-table-adapter.ts'
-export const usersTable = createTable({
+export const usersTable = table({
name: 'users',
columns: {
- id: number(),
- username: string(),
- email: string(),
- password_hash: string(),
- created_at: string(),
- updated_at: string(),
+ id: column.integer().primaryKey(),
+ username: column.text(),
+ email: column.text(),
+ password_hash: column.text(),
+ created_at: column.text(),
+ updated_at: column.text(),
},
primaryKey: 'id',
})
-export const passwordResetsTable = createTable({
+export const passwordResetsTable = table({
name: 'password_resets',
columns: {
- id: number(),
- user_id: number(),
- token_hash: string(),
- expires_at: number(),
- created_at: string(),
+ id: column.integer().primaryKey(),
+ user_id: column.integer(),
+ token_hash: column.text(),
+ expires_at: column.integer(),
+ created_at: column.text(),
},
primaryKey: 'id',
})
-export const chatThreadsTable = createTable({
+export const chatThreadsTable = table({
name: 'chat_threads',
columns: {
- id: string(),
- user_id: number(),
- title: string(),
- last_message_preview: string(),
- message_count: number(),
- created_at: string(),
- updated_at: string(),
- deleted_at: optional(nullable(string())),
+ id: column.text().primaryKey(),
+ user_id: column.integer(),
+ title: column.text(),
+ last_message_preview: column.text(),
+ message_count: column.integer(),
+ created_at: column.text(),
+ updated_at: column.text(),
+ deleted_at: column.text().nullable(),
},
primaryKey: 'id',
timestamps: {