From 63820f30603b6be6a0ceaedd9c1d335a43b9d720 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Apr 2026 17:35:47 +0000 Subject: [PATCH 1/7] Refresh Remix alpha 6 agent docs Co-authored-by: Kent C. Dodds --- docs/agents/remix/assert/changelog.md | 22 + docs/agents/remix/assert/index.md | 72 ++ docs/agents/remix/assets/changelog.md | 52 + docs/agents/remix/assets/index.md | 375 +++++++ docs/agents/remix/async-context-middleware.md | 58 -- .../async-context-middleware/changelog.md | 53 + .../remix/async-context-middleware/index.md | 119 +++ .../agents/remix/auth-middleware/changelog.md | 34 + docs/agents/remix/auth-middleware/index.md | 290 ++++++ docs/agents/remix/auth/changelog.md | 62 ++ docs/agents/remix/auth/index.md | 519 ++++++++++ docs/agents/remix/cli/changelog.md | 24 + docs/agents/remix/cli/index.md | 85 ++ docs/agents/remix/component/animate-basics.md | 113 -- docs/agents/remix/component/animate-layout.md | 172 --- .../remix/component/animate-patterns.md | 172 --- docs/agents/remix/component/animate-tips.md | 40 - docs/agents/remix/component/components.md | 141 --- .../remix/component/composition-basics.md | 121 --- .../remix/component/composition-keys.md | 99 -- docs/agents/remix/component/events-basics.md | 105 -- .../remix/component/events-best-practices.md | 96 -- .../agents/remix/component/events-patterns.md | 164 --- .../agents/remix/component/getting-started.md | 93 -- docs/agents/remix/component/handle-context.md | 41 - docs/agents/remix/component/handle-signals.md | 76 -- docs/agents/remix/component/handle-updates.md | 193 ---- docs/agents/remix/component/index.md | 52 - .../remix/component/interactions-basics.md | 166 --- .../remix/component/interactions-examples.md | 113 -- .../remix/component/patterns-data-loading.md | 127 --- .../component/patterns-focus-and-scroll.md | 107 -- .../agents/remix/component/patterns-inputs.md | 81 -- docs/agents/remix/component/patterns-setup.md | 148 --- docs/agents/remix/component/patterns-state.md | 125 --- .../remix/component/readme-components.md | 103 -- docs/agents/remix/component/readme-events.md | 62 -- docs/agents/remix/component/readme-extras.md | 40 - .../remix/component/readme-handle-api.md | 145 --- .../remix/component/readme-handle-context.md | 101 -- .../component/readme-handle-listeners.md | 69 -- .../agents/remix/component/readme-overview.md | 58 -- .../component/readme-styling-and-connect.md | 158 --- .../agents/remix/component/spring-advanced.md | 60 -- docs/agents/remix/component/spring-basics.md | 62 -- docs/agents/remix/component/styling-basics.md | 84 -- .../agents/remix/component/styling-nesting.md | 105 -- .../remix/component/styling-responsive.md | 94 -- .../remix/component/styling-selectors.md | 112 -- docs/agents/remix/component/testing.md | 58 -- docs/agents/remix/component/tween-advanced.md | 41 - docs/agents/remix/component/tween-basics.md | 38 - .../remix/compression-middleware/changelog.md | 57 + .../remix/compression-middleware/index.md | 188 +++- .../remix/compression-middleware/options.md | 167 --- docs/agents/remix/cookie/changelog.md | 67 ++ .../remix/{cookie.md => cookie/index.md} | 28 +- docs/agents/remix/cop-middleware/changelog.md | 35 + docs/agents/remix/cop-middleware/index.md | 144 +++ .../agents/remix/cors-middleware/changelog.md | 35 + docs/agents/remix/cors-middleware/index.md | 200 ++++ .../agents/remix/csrf-middleware/changelog.md | 35 + docs/agents/remix/csrf-middleware/index.md | 128 +++ docs/agents/remix/data-schema.md | 65 -- docs/agents/remix/data-schema/changelog.md | 37 + docs/agents/remix/data-schema/index.md | 509 +++++++++ docs/agents/remix/data-table-mysql.md | 47 - .../remix/data-table-mysql/changelog.md | 73 ++ docs/agents/remix/data-table-mysql/index.md | 93 ++ docs/agents/remix/data-table-postgres.md | 48 - .../remix/data-table-postgres/changelog.md | 79 ++ .../agents/remix/data-table-postgres/index.md | 84 ++ docs/agents/remix/data-table-sqlite.md | 50 - .../remix/data-table-sqlite/changelog.md | 83 ++ docs/agents/remix/data-table-sqlite/index.md | 98 ++ docs/agents/remix/data-table.md | 86 -- docs/agents/remix/data-table/changelog.md | 117 +++ docs/agents/remix/data-table/index.md | 651 ++++++++++++ docs/agents/remix/fetch-proxy.md | 76 -- docs/agents/remix/fetch-proxy/changelog.md | 65 ++ docs/agents/remix/fetch-proxy/index.md | 54 + .../remix/fetch-router/advanced-topics.md | 125 --- docs/agents/remix/fetch-router/changelog.md | 848 +++++++++++++++ docs/agents/remix/fetch-router/index.md | 954 ++++++++++++++++- docs/agents/remix/fetch-router/middleware.md | 130 --- .../remix/fetch-router/routing-methods.md | 173 ---- .../remix/fetch-router/routing-resources.md | 186 ---- .../remix/fetch-router/testing-and-related.md | 56 - docs/agents/remix/fetch-router/usage.md | 170 --- docs/agents/remix/file-storage-s3.md | 44 - .../agents/remix/file-storage-s3/changelog.md | 31 + docs/agents/remix/file-storage-s3/index.md | 57 + docs/agents/remix/file-storage/changelog.md | 197 ++++ .../index.md} | 33 +- docs/agents/remix/form-data-middleware.md | 99 -- .../remix/form-data-middleware/changelog.md | 79 ++ .../remix/form-data-middleware/index.md | 123 +++ .../remix/form-data-parser/changelog.md | 171 +++ .../index.md} | 60 +- docs/agents/remix/fs/changelog.md | 72 ++ docs/agents/remix/{fs.md => fs/index.md} | 24 +- docs/agents/remix/headers/accept-headers.md | 131 --- docs/agents/remix/headers/changelog.md | 461 ++++++++ .../remix/headers/conditional-headers.md | 192 ---- docs/agents/remix/headers/content-headers.md | 161 --- docs/agents/remix/headers/cookie-headers.md | 104 -- docs/agents/remix/headers/index.md | 624 ++++++++++- docs/agents/remix/headers/raw-headers.md | 34 - docs/agents/remix/html-template/changelog.md | 26 + .../index.md} | 37 +- docs/agents/remix/index.md | 373 ++++--- .../interaction/containers-and-disposal.md | 86 -- .../remix/interaction/custom-interactions.md | 120 --- docs/agents/remix/interaction/index.md | 45 - docs/agents/remix/interaction/listeners.md | 138 --- docs/agents/remix/lazy-file/changelog.md | 193 ++++ .../{lazy-file.md => lazy-file/index.md} | 22 +- .../remix/logger-middleware/changelog.md | 61 ++ .../index.md} | 54 +- .../method-override-middleware/changelog.md | 51 + .../index.md} | 29 +- docs/agents/remix/mime/changelog.md | 53 + docs/agents/remix/{mime.md => mime/index.md} | 36 +- .../remix/multipart-parser/benchmarks.md | 91 -- .../remix/multipart-parser/changelog.md | 266 +++++ docs/agents/remix/multipart-parser/index.md | 280 ++++- .../remix/multipart-parser/limits-and-node.md | 78 -- .../remix/multipart-parser/low-level.md | 40 - .../remix/node-fetch-server/advanced-usage.md | 58 -- .../remix/node-fetch-server/changelog.md | 118 +++ .../node-fetch-server/demos-and-benchmark.md | 35 - docs/agents/remix/node-fetch-server/index.md | 351 ++++++- .../remix/node-fetch-server/migration.md | 46 - .../remix/node-fetch-server/quick-start.md | 193 ---- docs/agents/remix/release-notes.md | 980 ++++++++++++++++++ docs/agents/remix/remix.md | 82 -- docs/agents/remix/remix/changelog.md | 287 +++++ docs/agents/remix/remix/index.md | 59 ++ docs/agents/remix/response/changelog.md | 100 ++ .../remix/response/compress-responses.md | 72 -- docs/agents/remix/response/file-responses.md | 108 -- docs/agents/remix/response/html-responses.md | 33 - docs/agents/remix/response/index.md | 280 ++++- .../remix/response/redirect-responses.md | 32 - docs/agents/remix/response/related.md | 25 - docs/agents/remix/route-pattern/changelog.md | 848 +++++++++++++++ .../index.md} | 81 +- .../remix/session-middleware/changelog.md | 66 ++ .../index.md} | 49 +- docs/agents/remix/session-storage-memcache.md | 46 - .../session-storage-memcache/changelog.md | 22 + .../remix/session-storage-memcache/index.md | 44 + docs/agents/remix/session-storage-redis.md | 43 - .../remix/session-storage-redis/changelog.md | 18 + .../remix/session-storage-redis/index.md | 40 + docs/agents/remix/session/changelog.md | 80 ++ .../remix/session/flash-and-security.md | 91 -- docs/agents/remix/session/index.md | 179 +++- docs/agents/remix/session/related.md | 23 - .../remix/session/storage-strategies.md | 55 - .../remix/static-middleware/changelog.md | 124 +++ .../index.md} | 37 +- docs/agents/remix/tar-parser/changelog.md | 55 + .../{tar-parser.md => tar-parser/index.md} | 44 +- docs/agents/remix/terminal/changelog.md | 19 + docs/agents/remix/terminal/index.md | 107 ++ docs/agents/remix/test/changelog.md | 69 ++ docs/agents/remix/test/index.md | 468 +++++++++ docs/agents/remix/ui/changelog.md | 106 ++ .../remix/ui/docs/component-changelog.md | 821 +++++++++++++++ docs/agents/remix/ui/docs/component.md | 786 ++++++++++++++ docs/agents/remix/ui/docs/components.md | 128 +++ docs/agents/remix/ui/docs/composition.md | 200 ++++ .../remix/{component => ui/docs}/context.md | 63 +- docs/agents/remix/ui/docs/events.md | 337 ++++++ docs/agents/remix/ui/docs/frames.md | 222 ++++ docs/agents/remix/ui/docs/getting-started.md | 179 ++++ docs/agents/remix/ui/docs/handle.md | 343 ++++++ docs/agents/remix/ui/docs/hydration.md | 135 +++ docs/agents/remix/ui/docs/interactions.md | 190 ++++ docs/agents/remix/ui/docs/patterns.md | 575 ++++++++++ docs/agents/remix/ui/docs/server-rendering.md | 115 ++ docs/agents/remix/ui/docs/spring.md | 286 +++++ docs/agents/remix/ui/docs/styling.md | 541 ++++++++++ docs/agents/remix/ui/docs/testing.md | 112 ++ docs/agents/remix/ui/docs/tween.md | 221 ++++ docs/agents/remix/ui/index.md | 202 ++++ docs/agents/remix/update.md | 70 -- 188 files changed, 19218 insertions(+), 8398 deletions(-) create mode 100644 docs/agents/remix/assert/changelog.md create mode 100644 docs/agents/remix/assert/index.md create mode 100644 docs/agents/remix/assets/changelog.md create mode 100644 docs/agents/remix/assets/index.md delete mode 100644 docs/agents/remix/async-context-middleware.md create mode 100644 docs/agents/remix/async-context-middleware/changelog.md create mode 100644 docs/agents/remix/async-context-middleware/index.md create mode 100644 docs/agents/remix/auth-middleware/changelog.md create mode 100644 docs/agents/remix/auth-middleware/index.md create mode 100644 docs/agents/remix/auth/changelog.md create mode 100644 docs/agents/remix/auth/index.md create mode 100644 docs/agents/remix/cli/changelog.md create mode 100644 docs/agents/remix/cli/index.md delete mode 100644 docs/agents/remix/component/animate-basics.md delete mode 100644 docs/agents/remix/component/animate-layout.md delete mode 100644 docs/agents/remix/component/animate-patterns.md delete mode 100644 docs/agents/remix/component/animate-tips.md delete mode 100644 docs/agents/remix/component/components.md delete mode 100644 docs/agents/remix/component/composition-basics.md delete mode 100644 docs/agents/remix/component/composition-keys.md delete mode 100644 docs/agents/remix/component/events-basics.md delete mode 100644 docs/agents/remix/component/events-best-practices.md delete mode 100644 docs/agents/remix/component/events-patterns.md delete mode 100644 docs/agents/remix/component/getting-started.md delete mode 100644 docs/agents/remix/component/handle-context.md delete mode 100644 docs/agents/remix/component/handle-signals.md delete mode 100644 docs/agents/remix/component/handle-updates.md delete mode 100644 docs/agents/remix/component/index.md delete mode 100644 docs/agents/remix/component/interactions-basics.md delete mode 100644 docs/agents/remix/component/interactions-examples.md delete mode 100644 docs/agents/remix/component/patterns-data-loading.md delete mode 100644 docs/agents/remix/component/patterns-focus-and-scroll.md delete mode 100644 docs/agents/remix/component/patterns-inputs.md delete mode 100644 docs/agents/remix/component/patterns-setup.md delete mode 100644 docs/agents/remix/component/patterns-state.md delete mode 100644 docs/agents/remix/component/readme-components.md delete mode 100644 docs/agents/remix/component/readme-events.md delete mode 100644 docs/agents/remix/component/readme-extras.md delete mode 100644 docs/agents/remix/component/readme-handle-api.md delete mode 100644 docs/agents/remix/component/readme-handle-context.md delete mode 100644 docs/agents/remix/component/readme-handle-listeners.md delete mode 100644 docs/agents/remix/component/readme-overview.md delete mode 100644 docs/agents/remix/component/readme-styling-and-connect.md delete mode 100644 docs/agents/remix/component/spring-advanced.md delete mode 100644 docs/agents/remix/component/spring-basics.md delete mode 100644 docs/agents/remix/component/styling-basics.md delete mode 100644 docs/agents/remix/component/styling-nesting.md delete mode 100644 docs/agents/remix/component/styling-responsive.md delete mode 100644 docs/agents/remix/component/styling-selectors.md delete mode 100644 docs/agents/remix/component/testing.md delete mode 100644 docs/agents/remix/component/tween-advanced.md delete mode 100644 docs/agents/remix/component/tween-basics.md create mode 100644 docs/agents/remix/compression-middleware/changelog.md delete mode 100644 docs/agents/remix/compression-middleware/options.md create mode 100644 docs/agents/remix/cookie/changelog.md rename docs/agents/remix/{cookie.md => cookie/index.md} (81%) create mode 100644 docs/agents/remix/cop-middleware/changelog.md create mode 100644 docs/agents/remix/cop-middleware/index.md create mode 100644 docs/agents/remix/cors-middleware/changelog.md create mode 100644 docs/agents/remix/cors-middleware/index.md create mode 100644 docs/agents/remix/csrf-middleware/changelog.md create mode 100644 docs/agents/remix/csrf-middleware/index.md delete mode 100644 docs/agents/remix/data-schema.md create mode 100644 docs/agents/remix/data-schema/changelog.md create mode 100644 docs/agents/remix/data-schema/index.md delete mode 100644 docs/agents/remix/data-table-mysql.md create mode 100644 docs/agents/remix/data-table-mysql/changelog.md create mode 100644 docs/agents/remix/data-table-mysql/index.md delete mode 100644 docs/agents/remix/data-table-postgres.md create mode 100644 docs/agents/remix/data-table-postgres/changelog.md create mode 100644 docs/agents/remix/data-table-postgres/index.md delete mode 100644 docs/agents/remix/data-table-sqlite.md create mode 100644 docs/agents/remix/data-table-sqlite/changelog.md create mode 100644 docs/agents/remix/data-table-sqlite/index.md delete mode 100644 docs/agents/remix/data-table.md create mode 100644 docs/agents/remix/data-table/changelog.md create mode 100644 docs/agents/remix/data-table/index.md delete mode 100644 docs/agents/remix/fetch-proxy.md create mode 100644 docs/agents/remix/fetch-proxy/changelog.md create mode 100644 docs/agents/remix/fetch-proxy/index.md delete mode 100644 docs/agents/remix/fetch-router/advanced-topics.md create mode 100644 docs/agents/remix/fetch-router/changelog.md delete mode 100644 docs/agents/remix/fetch-router/middleware.md delete mode 100644 docs/agents/remix/fetch-router/routing-methods.md delete mode 100644 docs/agents/remix/fetch-router/routing-resources.md delete mode 100644 docs/agents/remix/fetch-router/testing-and-related.md delete mode 100644 docs/agents/remix/fetch-router/usage.md delete mode 100644 docs/agents/remix/file-storage-s3.md create mode 100644 docs/agents/remix/file-storage-s3/changelog.md create mode 100644 docs/agents/remix/file-storage-s3/index.md create mode 100644 docs/agents/remix/file-storage/changelog.md rename docs/agents/remix/{file-storage.md => file-storage/index.md} (62%) delete mode 100644 docs/agents/remix/form-data-middleware.md create mode 100644 docs/agents/remix/form-data-middleware/changelog.md create mode 100644 docs/agents/remix/form-data-middleware/index.md create mode 100644 docs/agents/remix/form-data-parser/changelog.md rename docs/agents/remix/{form-data-parser.md => form-data-parser/index.md} (72%) create mode 100644 docs/agents/remix/fs/changelog.md rename docs/agents/remix/{fs.md => fs/index.md} (79%) delete mode 100644 docs/agents/remix/headers/accept-headers.md create mode 100644 docs/agents/remix/headers/changelog.md delete mode 100644 docs/agents/remix/headers/conditional-headers.md delete mode 100644 docs/agents/remix/headers/content-headers.md delete mode 100644 docs/agents/remix/headers/cookie-headers.md delete mode 100644 docs/agents/remix/headers/raw-headers.md create mode 100644 docs/agents/remix/html-template/changelog.md rename docs/agents/remix/{html-template.md => html-template/index.md} (69%) delete mode 100644 docs/agents/remix/interaction/containers-and-disposal.md delete mode 100644 docs/agents/remix/interaction/custom-interactions.md delete mode 100644 docs/agents/remix/interaction/index.md delete mode 100644 docs/agents/remix/interaction/listeners.md create mode 100644 docs/agents/remix/lazy-file/changelog.md rename docs/agents/remix/{lazy-file.md => lazy-file/index.md} (90%) create mode 100644 docs/agents/remix/logger-middleware/changelog.md rename docs/agents/remix/{logger-middleware.md => logger-middleware/index.md} (61%) create mode 100644 docs/agents/remix/method-override-middleware/changelog.md rename docs/agents/remix/{method-override-middleware.md => method-override-middleware/index.md} (70%) create mode 100644 docs/agents/remix/mime/changelog.md rename docs/agents/remix/{mime.md => mime/index.md} (73%) delete mode 100644 docs/agents/remix/multipart-parser/benchmarks.md create mode 100644 docs/agents/remix/multipart-parser/changelog.md delete mode 100644 docs/agents/remix/multipart-parser/limits-and-node.md delete mode 100644 docs/agents/remix/multipart-parser/low-level.md delete mode 100644 docs/agents/remix/node-fetch-server/advanced-usage.md create mode 100644 docs/agents/remix/node-fetch-server/changelog.md delete mode 100644 docs/agents/remix/node-fetch-server/demos-and-benchmark.md delete mode 100644 docs/agents/remix/node-fetch-server/migration.md delete mode 100644 docs/agents/remix/node-fetch-server/quick-start.md create mode 100644 docs/agents/remix/release-notes.md delete mode 100644 docs/agents/remix/remix.md create mode 100644 docs/agents/remix/remix/changelog.md create mode 100644 docs/agents/remix/remix/index.md create mode 100644 docs/agents/remix/response/changelog.md delete mode 100644 docs/agents/remix/response/compress-responses.md delete mode 100644 docs/agents/remix/response/file-responses.md delete mode 100644 docs/agents/remix/response/html-responses.md delete mode 100644 docs/agents/remix/response/redirect-responses.md delete mode 100644 docs/agents/remix/response/related.md create mode 100644 docs/agents/remix/route-pattern/changelog.md rename docs/agents/remix/{route-pattern.md => route-pattern/index.md} (69%) create mode 100644 docs/agents/remix/session-middleware/changelog.md rename docs/agents/remix/{session-middleware.md => session-middleware/index.md} (60%) delete mode 100644 docs/agents/remix/session-storage-memcache.md create mode 100644 docs/agents/remix/session-storage-memcache/changelog.md create mode 100644 docs/agents/remix/session-storage-memcache/index.md delete mode 100644 docs/agents/remix/session-storage-redis.md create mode 100644 docs/agents/remix/session-storage-redis/changelog.md create mode 100644 docs/agents/remix/session-storage-redis/index.md create mode 100644 docs/agents/remix/session/changelog.md delete mode 100644 docs/agents/remix/session/flash-and-security.md delete mode 100644 docs/agents/remix/session/related.md delete mode 100644 docs/agents/remix/session/storage-strategies.md create mode 100644 docs/agents/remix/static-middleware/changelog.md rename docs/agents/remix/{static-middleware.md => static-middleware/index.md} (61%) create mode 100644 docs/agents/remix/tar-parser/changelog.md rename docs/agents/remix/{tar-parser.md => tar-parser/index.md} (62%) create mode 100644 docs/agents/remix/terminal/changelog.md create mode 100644 docs/agents/remix/terminal/index.md create mode 100644 docs/agents/remix/test/changelog.md create mode 100644 docs/agents/remix/test/index.md create mode 100644 docs/agents/remix/ui/changelog.md create mode 100644 docs/agents/remix/ui/docs/component-changelog.md create mode 100644 docs/agents/remix/ui/docs/component.md create mode 100644 docs/agents/remix/ui/docs/components.md create mode 100644 docs/agents/remix/ui/docs/composition.md rename docs/agents/remix/{component => ui/docs}/context.md (76%) create mode 100644 docs/agents/remix/ui/docs/events.md create mode 100644 docs/agents/remix/ui/docs/frames.md create mode 100644 docs/agents/remix/ui/docs/getting-started.md create mode 100644 docs/agents/remix/ui/docs/handle.md create mode 100644 docs/agents/remix/ui/docs/hydration.md create mode 100644 docs/agents/remix/ui/docs/interactions.md create mode 100644 docs/agents/remix/ui/docs/patterns.md create mode 100644 docs/agents/remix/ui/docs/server-rendering.md create mode 100644 docs/agents/remix/ui/docs/spring.md create mode 100644 docs/agents/remix/ui/docs/styling.md create mode 100644 docs/agents/remix/ui/docs/testing.md create mode 100644 docs/agents/remix/ui/docs/tween.md create mode 100644 docs/agents/remix/ui/index.md delete mode 100644 docs/agents/remix/update.md diff --git a/docs/agents/remix/assert/changelog.md b/docs/agents/remix/assert/changelog.md new file mode 100644 index 0000000..29c123a --- /dev/null +++ b/docs/agents/remix/assert/changelog.md @@ -0,0 +1,22 @@ +# assert changelog + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/assert`. + + A compatible subset of `node:assert/strict` that works in any JavaScript + environment, including browsers. Uses strict equality (`===`) for all + comparisons — no type coercion. + - `AssertionError` — compatible with `node:assert.AssertionError` (`actual`, + `expected`, `operator`, `name`) + - `assert.ok` — truthy check + - `assert.equal` / `assert.notEqual` — strict equality (`===` / `!==`) + - `assert.deepEqual` / `assert.notDeepEqual` — recursive strict deep equality + - `assert.match` — string matches a regexp + - `assert.fail` — unconditional failure + - `assert.throws` — synchronous throw assertion + - `assert.rejects` — async rejection assertion + +## Unreleased diff --git a/docs/agents/remix/assert/index.md b/docs/agents/remix/assert/index.md new file mode 100644 index 0000000..e1a98ca --- /dev/null +++ b/docs/agents/remix/assert/index.md @@ -0,0 +1,72 @@ + + +# assert + +A compatible subset of `node:assert/strict` that works in any JavaScript +environment, including browsers. + +Uses strict equality (`===`) for all comparisons — no type coercion. + +## Features + +- `AssertionError` — compatible with `node:assert.AssertionError` (`actual`, + `expected`, `operator`, `name`) +- `assert.ok` — truthy check +- `assert.equal` / `assert.notEqual` — strict equality (`===` / `!==`) +- `assert.deepEqual` / `assert.notDeepEqual` — recursive strict deep equality +- `assert.match` — string matches a regexp +- `assert.fail` — unconditional failure +- `assert.throws` — synchronous throw assertion +- `assert.rejects` — async rejection assertion + +## Installation + +```sh +npm i remix +``` + +## Usage + +Mirrors `node:assert/strict` — uses strict equality (`===`), so `1 !== '1'` and +`null !== undefined`. + +```ts +import assert from 'remix/assert' + +assert.ok(true) +assert.equal(1, 1) +assert.equal(1, '1') // throws — different types +assert.notEqual('a', 'b') +assert.deepEqual({ a: 1 }, { a: 1 }) +assert.deepEqual({ a: 1 }, { a: '1' }) // throws — different types +assert.match('hello world', /world/) +assert.fail('should not reach here') + +await assert.rejects(() => Promise.reject(new Error('oops'))) +assert.throws(() => { + throw new TypeError('bad') +}, TypeError) +``` + +### Named exports + +Each assertion is also exported as a named function: + +```ts +import { + ok, + assert, // alias of ok() + equal, + notEqual, + deepEqual, + notDeepEqual, + match, + fail, + throws, + rejects, +} from 'remix/assert' +``` + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/assets/changelog.md b/docs/agents/remix/assets/changelog.md new file mode 100644 index 0000000..02182c2 --- /dev/null +++ b/docs/agents/remix/assets/changelog.md @@ -0,0 +1,52 @@ +# `assets` CHANGELOG + +This is the changelog for +[`assets`](https://github.com/remix-run/remix/tree/main/packages/assets). It +follows [semantic versioning](https://semver.org/). + +## v0.2.0 + +### Minor Changes + +- BREAKING CHANGE: `target` configuration is now configured at the top level + with an object format, supporting `es` version targets along with browser + version targets. + + Browser targets are configured with string versions such as + `target: { chrome: '109', safari: '16.4' }`, and scripts can specify `es` as a + year of `2015` or higher such as `target: { es: '2020' }`. + + To migrate existing script configuration, replace `scripts.target` options + like `scripts: { target: 'es2020' }` with `target: { es: '2020' }`. + +- BREAKING CHANGE: Shared compiler options are now provided at the top level of + `createAssetServer()`. Use `sourceMaps`, `sourceMapSourcePaths`, and `minify` + directly on the asset server options instead of being nested under `scripts`. + This allows these options to also be used for styles as well as scripts. + + To migrate existing configuration, move `scripts.minify`, + `scripts.sourceMaps`, `scripts.sourceMapSourcePaths` to the top-level asset + server options. + +- `createAssetServer()` now compiles and serves `.css` files alongside scripts, + including local `@import` rewriting, fingerprinting, and shared compiler + options for minification, source maps, and browser compatibility targeting. + +### Patch Changes + +- Fix matching of dot-prefixed files and directories in `allow` and `deny` globs + +- Improve asset server import errors to include the resolved file path when a + resolved import is later rejected by validation for allow/deny rules, + supported file types and `fileMap` configuration. + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/assets`. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`route-pattern@0.20.1`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.20.1) diff --git a/docs/agents/remix/assets/index.md b/docs/agents/remix/assets/index.md new file mode 100644 index 0000000..eb513a3 --- /dev/null +++ b/docs/agents/remix/assets/index.md @@ -0,0 +1,375 @@ + + +# assets + +Fetch-based server for compiling browser JS/TS and CSS assets on demand. + +## Features + +- **On-Demand Compilation** - Compile browser scripts and styles on demand +- **Custom File Mapping** - Define patterns for mapping public URLs to file + paths on disk +- **Access Control** - Control exactly which files can be served with allow and + deny rules +- **Preloads** - Generate preload URLs for scripts and styles based on imports +- **Caching** - Conservative caching by default with stable URLs, ETags, and + revalidation +- **Optional Fingerprinting** - Source-based fingerprinted URLs for long-lived + browser caching +- **Source Maps** - Serve inline or external sourcemaps + +## Installation + +```sh +npm i remix +``` + +## Usage + +Use `createAssetServer` to serve browser JS/TS and CSS assets from a URL +namespace in your app. + +```ts +import { createRouter } from 'remix/fetch-router' +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { + '/assets/app/*path': 'app/*path', + '/assets/npm/*path': 'node_modules/*path', + }, + allow: ['app/assets/**', 'node_modules/**'], +}) + +let router = createRouter() + +router.get('/assets/*', ({ request }) => { + return assetServer.fetch(request) +}) +``` + +This example gives you an `/assets/*` endpoint that serves compiled browser +assets from `app/assets` and `node_modules`. + +## Root Directory + +Use `rootDir` to specify the root directory of the asset server, which is used +to resolve relative file paths. Defaults to `process.cwd()`. + +```ts +import * as path from 'node:path' +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + rootDir: path.resolve(import.meta.dirname, '..'), + fileMap: { + '/assets/app/*path': 'app/*path', + '/assets/npm/*path': 'node_modules/*path', + }, + allow: ['app/assets/**', 'node_modules/**'], +}) +``` + +## Access Control + +You must provide an `allow` list to specify which files are allowed to be +served. `deny` is optional and takes precedence over `allow`. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + deny: ['app/**/*.server.*'], +}) +``` + +Rules for `allow` and `deny` are file paths or globs. Relative values are +resolved from `rootDir`. Absolute file paths match exactly, and absolute +directory paths also match their descendants. + +## File Map + +Use `fileMap` to map public URLs to file paths on disk. The keys are public URL +patterns, and the values are root-relative file path patterns. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { + '/assets/app/*path': 'app/*path', + '/assets/packages/*path': '../packages/*path', + }, + allow: ['app/assets/**', '../packages/**'], +}) +``` + +`fileMap` entries use +[`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern) +syntax for both URL and file patterns. Wildcards must be named, and the same +params must appear in both patterns so imports can be rewritten back to public +URLs. + +### File watching + +The file system is watched by default so source changes are picked up without +requiring a server restart. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**', 'app/node_modules/**'], +}) +``` + +When finished with the asset server, call `await assetServer.close()` to clean +up the file watcher. + +```ts +await assetServer.close() +``` + +You can disable file watching if the files on disk won't change, or if watching +is managed at a higher level (e.g. Node's `--watch` flag). + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**', 'app/node_modules/**'], + watch: false, +}) +``` + +You can optionally provide an array of glob patterns to the `watch.ignore` +option: + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**', 'app/node_modules/**'], + watch: { + ignore: ['**/node_modules/**'], + }, +}) +``` + +## Hrefs + +Use `assetServer.getHref()` when you need the public URL for a served asset. You +can provide a root-relative or absolute file path, or a `file://` URL. + +```ts +let src = await assetServer.getHref('app/assets/entry.tsx') +// '/assets/app/assets/entry.tsx' +``` + +## Preloads + +Use `assetServer.getPreloads()` when rendering HTML so you can turn the returned +URLs into ``, stylesheet preload tags, or `Link` +headers for one or more assets and their dependencies. You can provide +root-relative or absolute file paths, or `file://` URLs. + +```ts +let preloads = await assetServer.getPreloads([ + 'app/assets/entry.tsx', + 'app/assets/search.tsx', +]) +// [ +// '/assets/app/assets/entry.tsx', +// '/assets/app/assets/search.tsx', +// '/assets/app/assets/utils.ts', +// '/assets/npm/@remix-run/ui/index.js', +// ...etc +// ] +``` + +## Fingerprinting + +By default, assets are served at stable URLs with ETags and +`Cache-Control: no-cache`. Responses are cached for the lifetime of the asset +server instance. + +If you want clients to cache assets aggressively without revalidation, you can +opt into source-based fingerprinting. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + watch: false, + fingerprint: { + buildId: process.env.GITHUB_SHA, + }, +}) +``` + +When fingerprinting is enabled, assets use a `.@` segment before +the file extension and are served with +`Cache-Control: public, max-age=31536000, immutable`. + +Source fingerprints are based on the original file contents and the build ID. +The build ID must change for each deployment so that fingerprinted assets are +invalidated together. This fingerprinting strategy assumes that files on disk +won't change, so fingerprinting requires `watch: false`. + +## Target + +Use `target` to lower emitted syntax to a specific browser support policy and/or +ECMAScript version. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + target: { + chrome: '109', + ios: '15.6', + es: '2020', + }, +}) +``` + +Supported target options are `chrome`, `firefox`, `safari`, `edge`, `opera`, +`ios`, `samsung`, and `es` (ECMAScript version). + +### Source Maps + +Enable sourcemaps with either `'external'` or `'inline'` using `sourceMaps`: + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + sourceMaps: 'external', +}) +``` + +By default, sourcemap `sources` use URLs so they're presented alongside the +compiled output in your browser's developer tools. You can also use file system +paths instead with `sourceMapSourcePaths`: + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + sourceMaps: 'inline', + sourceMapSourcePaths: 'absolute', +}) +``` + +### Minification + +Enable minification with `minify`: + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + minify: true, +}) +``` + +## Script Options + +### Define + +Use `scripts.define` to replace global identifiers with constant expressions. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**', 'app/node_modules/**'], + scripts: { + define: { + 'process.env.NODE_ENV': '"production"', + }, + }, +}) +``` + +Values are injected exactly as defined, so string literals must include their +own quotes, e.g. `process.env.NODE_ENV` must be `"production"` rather than +`production`. + +### External Imports + +Use `scripts.external` to leave specific import specifiers unchanged by +providing an array of specifiers. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + scripts: { + external: ['my-external-import'], + }, +}) +``` + +## CSS Imports + +Relative CSS `@import` rules are rewritten to asset server URLs. External +`@import` URLs are left unchanged automatically. `url()` references are +preserved as authored. + +```css +/* Rewritten to asset server URL: */ +@import './reset.css'; +/* External URL: */ +@import 'https://fonts.googleapis.com/css2?family=Inter'; +``` + +## Error Handling + +Use `onError` to report unexpected compilation failures and/or return a custom +response. + +```ts +import { createAssetServer } from 'remix/assets' + +let assetServer = createAssetServer({ + fileMap: { '/assets/app/*path': 'app/*path' }, + allow: ['app/assets/**'], + onError(error) { + console.error('Failed to build client assets', error) + return new Response('Client asset build failed', { status: 500 }) + }, +}) +``` + +If `onError` returns nothing, the asset server responds with the default +`500 Internal Server Error` response. + +## Related Packages + +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + A Fetch-based router that pairs naturally with `assets` +- [`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern) - + Route-pattern syntax for URL and route file matching + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/async-context-middleware.md b/docs/agents/remix/async-context-middleware.md deleted file mode 100644 index 681abba..0000000 --- a/docs/agents/remix/async-context-middleware.md +++ /dev/null @@ -1,58 +0,0 @@ -# async-context-middleware - -Source: -https://github.com/remix-run/remix/tree/main/packages/async-context-middleware - -## README - -Middleware for storing request context in `AsyncLocalStorage` for use with -[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router). - -This middleware stores the request context in -[`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) -(using `node:async_hooks`), making it available to all functions in the same -async execution context. - -## Installation - -```sh -bun add @remix-run/async-context-middleware -``` - -## Usage - -Simply use the `asyncContext()` middleware at the router level to make the -request context available to all functions in the same async execution context. -Get access to the context using the `getContext()` function. - -```ts -import { createRouter } from '@remix-run/fetch-router' -import { asyncContext, getContext } from '@remix-run/async-context-middleware' - -let router = createRouter({ - middleware: [asyncContext()], -}) - -router.get('/users/:id', async () => { - // Access context from anywhere in the async call stack - let context = getContext() - let userId = context.params.id - - return new Response(`User ${userId}`) -}) -``` - -Note: This middleware requires support for `node:async_hooks`. - -## Related Packages - -- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - Router for the web Fetch API - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/async-context-middleware/changelog.md b/docs/agents/remix/async-context-middleware/changelog.md new file mode 100644 index 0000000..65caa94 --- /dev/null +++ b/docs/agents/remix/async-context-middleware/changelog.md @@ -0,0 +1,53 @@ +# `async-context-middleware` CHANGELOG + +This is the changelog for +[`async-context-middleware`](https://github.com/remix-run/remix/tree/main/packages/async-context-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.2.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.2.0 + +### Minor Changes + +- `getContext()` can now be typed per app by augmenting `AsyncContextTypes`, + which makes `asyncContext()` work cleanly with app-specific `fetch-router` + request context contracts. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.1.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + +## v0.1.2 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + +## v0.1.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.1.0 (2025-11-19) + +Initial release extracted from `@remix-run/fetch-router` v0.9.0. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/async-context-middleware/README.md) +for more details. diff --git a/docs/agents/remix/async-context-middleware/index.md b/docs/agents/remix/async-context-middleware/index.md new file mode 100644 index 0000000..817e9fe --- /dev/null +++ b/docs/agents/remix/async-context-middleware/index.md @@ -0,0 +1,119 @@ + + +# async-context-middleware + +Request-scoped async context middleware for Remix. It stores each request +context in +[`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) +so utilities can access it anywhere in the same async call stack. + +## Features + +- **Request context access** - Read the current `RequestContext` from anywhere + in the same async execution flow +- **App-typed `getContext()`** - Augment `AsyncContextTypes` so `getContext()` + returns your app's request context contract +- **Simple router integration** - Add a single middleware at the router level +- **Node async hooks** - Built on `node:async_hooks` `AsyncLocalStorage` + +## Installation + +```sh +npm i remix +``` + +## Usage + +Use `asyncContext()` at the router level to make the current request context +available to helpers deeper in the same async call stack. + +```ts +import { createRouter } from 'remix/fetch-router' +import { asyncContext, getContext } from 'remix/async-context-middleware' + +let router = createRouter({ + middleware: [asyncContext()], +}) + +async function loadCurrentUser() { + let context = getContext() + let userId = context.params.id + + return users.getById(userId) +} + +router.get('/users/:id', async () => { + let user = await loadCurrentUser() + return Response.json(user) +}) +``` + +This middleware requires support for `node:async_hooks`, so it is intended for +Node.js runtimes. + +## Typed `getContext()` + +`getContext()` is global and out-of-band, so apps can augment +`AsyncContextTypes` to tell the package what request context lives in async +local storage. + +```ts +import type { + AnyParams, + MiddlewareContext, + WithParams, +} from 'remix/fetch-router' +import type { WithRequiredAuth } from 'remix/auth-middleware' + +export type RootMiddleware = [ + ReturnType, + ReturnType, +] + +export type AppContext = WithParams< + MiddlewareContext, + params +> + +export type AuthenticatedAppContext = + WithRequiredAuth, { id: string }> + +declare module 'remix/async-context-middleware' { + interface AsyncContextTypes { + requestContext: AppContext + } +} +``` + +After that augmentation, `getContext()` returns `AppContext` +everywhere in the app. + +```ts +import { Auth } from 'remix/auth-middleware' +import { getContext } from 'remix/async-context-middleware' + +function getCurrentAuth() { + return getContext().get(Auth) +} +``` + +Use a broad app-level context like `AppContext` here. Route handlers +themselves can still use more precise route-specific params in their own +`RequestContext` types. + +## Related Packages + +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + Router and request context contracts for Remix +- [`auth-middleware`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) - + Request-time auth state and protected route middleware +- [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - + Session loading middleware often paired with async request context + +## Related Work + +- [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/auth-middleware/changelog.md b/docs/agents/remix/auth-middleware/changelog.md new file mode 100644 index 0000000..d1c4d3a --- /dev/null +++ b/docs/agents/remix/auth-middleware/changelog.md @@ -0,0 +1,34 @@ +# `auth-middleware` CHANGELOG + +This is the changelog for +[`auth-middleware`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.1.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.0 + +### Minor Changes + +- Add `auth-middleware`, a pluggable authentication middleware package for + `fetch-router`. + + Includes: + - the `Auth` context key and `AuthState` for reading request auth state with + `context.get(Auth)` + - `auth()` for resolving request authentication state with `context.get(Auth)` + - `requireAuth()` for enforcing authenticated access with configurable failure + responses + - `WithAuth` and `WithRequiredAuth` for app-level request context contracts + - built-in `createBearerTokenAuthScheme()`, `createAPIAuthScheme()`, and + `createSessionAuthScheme()` helpers + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) diff --git a/docs/agents/remix/auth-middleware/index.md b/docs/agents/remix/auth-middleware/index.md new file mode 100644 index 0000000..6a44884 --- /dev/null +++ b/docs/agents/remix/auth-middleware/index.md @@ -0,0 +1,290 @@ + + +# auth-middleware + +Request-time authentication and route protection for Remix. Use this package to +resolve identity into `context.get(Auth)` from sessions, bearer tokens, API +keys, or your own schemes. Pair it with +[`remix/auth`](https://github.com/remix-run/remix/tree/main/packages/auth) when +you need browser login routes that call `verifyCredentials()` or +`finishExternalAuth()`, then rotate the session id with `completeAuth()` before +writing the auth record. + +## Features + +- Request auth resolution without mutating request objects +- Route protection with `requireAuth()` and configurable failure behavior +- Built-in auth schemes for sessions, bearer tokens, and API keys +- Ordered fallback across multiple auth schemes +- Public and private route support with the same resolved auth state +- Designed to pair with browser login flows that persist session auth records + earlier in the request lifecycle + +## Installation + +```sh +npm i remix +``` + +## Usage + +The following example shows the request-time half of a session-backed browser +login flow: + +- another part of the app has already called `completeAuth()` and written + `{ userId }` into the returned session +- `remix/auth-middleware` reads that value, resolves the current user, and + protects the dashboard route + +```ts +import { + auth, + Auth, + createSessionAuthScheme, + requireAuth, +} from 'remix/auth-middleware' +import { createRouter } from 'remix/fetch-router' +import { route } from 'remix/fetch-router/routes' +import type { GoodAuth } from 'remix/auth-middleware' +import { session } from 'remix/session-middleware' + +let routes = route({ + app: { + dashboard: '/dashboard', + }, +}) + +let router = createRouter({ + middleware: [ + session(sessionCookie, sessionStorage), + auth({ + schemes: [ + createSessionAuthScheme({ + read(session) { + return session.get('auth') as { userId: string } | null + }, + verify(value) { + return users.getById(value.userId) + }, + invalidate(session) { + session.unset('auth') + }, + }), + ], + }), + ], +}) + +router.get(routes.app.dashboard, { + middleware: [requireAuth()], + handler(context) { + let auth = context.get(Auth) as GoodAuth<{ id: string; email: string }> + + return Response.json({ + id: auth.identity.id, + email: auth.identity.email, + method: auth.method, + }) + }, +}) +``` + +In this example, `createSessionAuthScheme()` turns a persisted session auth +record back into request auth state, `auth()` stores that state at +`context.get(Auth)`, and `requireAuth()` rejects anonymous requests. + +If you need to create the login route, start an OAuth redirect, finish a +provider callback, or write the session auth record in the first place, use +[`remix/auth`](https://github.com/remix-run/remix/tree/main/packages/auth): + +- `verifyCredentials()` for direct credentials flows +- `startExternalAuth()` and `finishExternalAuth()` for OAuth and OIDC flows +- `completeAuth()` to rotate the session id before writing the auth record that + this package reads later + +## Route Protection + +This package includes two middlewares: + +- `auth()` to resolve auth state and store it in `context.get(Auth)` +- `requireAuth()` to reject requests that aren't authenticated + +That separation is intentional so the same auth resolution can support public +routes, API routes, and browser routes with different failure behavior. + +`auth()` resolves auth state and stores either `{ ok: true, identity, method }` +or `{ ok: false, error? }` in `context.get(Auth)`. + +Use `requireAuth()` after `auth()` when a route must be authenticated. If +`auth()` did not run first, `requireAuth()` throws. Otherwise it returns +`401 Unauthorized` by default, or you can replace that with +`onFailure(context, auth)` to return JSON, redirects, or any other custom +response. + +Auth challenges are forwarded to `WWW-Authenticate` automatically when the auth +failure included a `challenge`, so clients that honor those challenges can react +without custom header handling. + +## Auth Schemes + +An `AuthScheme` is any object with a `name` and an `authenticate(context)` +method. The `auth()` middleware tries each scheme in order until one returns a +success or failure result. If no scheme returns success or failure, the request +is treated as anonymous. + +This package ships with three built-in auth schemes: + +- `createBearerTokenAuthScheme()` for bearer tokens in the + [HTTP `Authorization: Bearer ` header](https://datatracker.ietf.org/doc/html/rfc6750#section-2.1) +- `createAPIAuthScheme()` for API keys in a custom request header +- `createSessionAuthScheme()` for session-backed auth loaded by + [a `session()` middleware](https://github.com/remix-run/remix/tree/main/packages/session-middleware) + +## Custom Auth Schemes + +If none of the built-in auth schemes match your environment, you can create your +own auth scheme easily. A custom scheme usually wraps one auth mechanism behind +a small `create*` factory function and returns an `AuthScheme`. For example, +apps behind a trusted access proxy can authenticate requests from forwarded +identity headers instead of sessions or bearer tokens. + +```ts +import type { RequestContext } from 'remix/fetch-router' +import type { AuthScheme } from 'remix/auth-middleware' + +type User = { + id: string + role: 'admin' | 'user' +} + +function createTrustedProxyAuthScheme(): AuthScheme { + return { + name: 'trusted-proxy', + async authenticate(context: RequestContext) { + let email = context.headers.get('X-Forwarded-Email') + + if (email == null) { + return + } + + let user = await users.getByEmail(email) + + if (user == null) { + return { + status: 'failure', + code: 'invalid_credentials', + message: 'Unknown forwarded user', + } + } + + return { + status: 'success', + identity: user, + } + }, + } +} +``` + +Only use a scheme like this when the app is reachable exclusively through +infrastructure you trust to set the headers you rely on. In this case, the +`X-Forwarded-Email` header. + +`authenticate(context)` can return: + +- `null`, `undefined`, or no return value to skip this scheme +- `{ status: 'success', identity }` to authenticate the request +- `{ status: 'failure', code?, message?, challenge? }` to stop with an auth + error + +The scheme `name` becomes `auth.method` when authentication succeeds. + +## Simple Auth Cookies + +If your app already has an auth cookie and you do not need a session-backed +identity lookup, you can use a small custom auth scheme and still rely on +`requireAuth()` for route protection. + +```ts +import { auth, requireAuth } from 'remix/auth-middleware' +import type { AuthScheme } from 'remix/auth-middleware' +import { createCookie } from 'remix/cookie' +import { createRouter } from 'remix/fetch-router' +import { redirect } from 'remix/response/redirect' + +let authCookie = createCookie('__auth', { + httpOnly: true, + sameSite: 'lax', + path: '/', +}) + +let authCookieScheme: AuthScheme<'demo-user'> = { + name: 'auth-cookie', + async authenticate(context) { + let value = await authCookie.parse(context.headers.get('cookie')) + if (value !== '1') { + return + } + + return { + status: 'success', + identity: 'demo-user', + } + }, +} + +let requireAuthCookie = requireAuth<'demo-user'>({ + onFailure(context) { + let isFrameRequest = context.request.headers.get('x-remix-frame') === 'true' + if (isFrameRequest) { + return new Response('

Not authorized

', { + status: 401, + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }) + } + + return redirect('/login') + }, +}) + +let router = createRouter({ + middleware: [ + auth({ + schemes: [authCookieScheme], + }), + ], +}) + +router.get('/dashboard', { + middleware: [requireAuthCookie], + handler() { + return new Response('ok') + }, +}) +``` + +This pattern keeps the auth check app-owned. Use +[`remix/session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) +and [`remix/auth`](https://github.com/remix-run/remix/tree/main/packages/auth) +when you need server-managed session data, credential verification helpers, or +OAuth/OIDC flows. + +## Related Packages + +- [`auth`](https://github.com/remix-run/remix/tree/main/packages/auth) - Browser + auth primitives for credentials, OAuth, and OIDC flows +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + Router and middleware runtime +- [`response`](https://github.com/remix-run/remix/tree/main/packages/response) - + Response helpers like redirects + +## Related Work + +- [HTTP Authentication Framework](https://datatracker.ietf.org/doc/html/rfc7235) +- [OAuth 2.0 Bearer Token Usage](https://datatracker.ietf.org/doc/html/rfc6750) + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/auth/changelog.md b/docs/agents/remix/auth/changelog.md new file mode 100644 index 0000000..6162644 --- /dev/null +++ b/docs/agents/remix/auth/changelog.md @@ -0,0 +1,62 @@ +# `auth` CHANGELOG + +This is the changelog for +[`auth`](https://github.com/remix-run/remix/tree/main/packages/auth). It follows +[semantic versioning](https://semver.org/). + +## v0.2.0 + +### Minor Changes + +- Added `createAtmosphereAuthProvider(options)` to support atproto OAuth flows + against Atmosphere-compatible authorization servers. + + The new provider resolves handles and DIDs with + `provider.prepare(handleOrDid)` before redirecting, performs required pushed + authorization requests with DPoP, supports both public web clients and + localhost loopback development clients, and seals per-session DPoP state into + the in-flight OAuth transaction using the required `sessionSecret` option + instead of a separate persistent store. + + Create the Atmosphere provider once with shared options, call + `provider.prepare(handleOrDid)` only before `startExternalAuth()`, and pass + the module-scope provider directly to `finishExternalAuth()` and + `refreshExternalAuth()`. Atmosphere callback results preserve the DPoP binding + state and authorization server refresh details alongside the returned + `accessToken` and `refreshToken`, so callers can reuse the completed token + bundle directly for refresh-token exchange and follow-up DPoP-signed requests. + +- Added `refreshExternalAuth()` to `@remix-run/auth` so apps can exchange stored + refresh tokens for fresh OAuth and OIDC token bundles. + + The built-in OIDC providers, X, and Atmosphere now implement refresh-token + exchange. Refreshed token bundles preserve the existing refresh token when the + provider omits a rotated value. + +## v0.1.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.0 + +### Minor Changes + +- Add `auth`, a high-level browser authentication package for Remix. + + Includes: + - generic `oidc()` support for standards-based providers + - thin `microsoft()`, `okta()`, and `auth0()` wrappers on top of OIDC + - OAuth provider helpers for Google, GitHub, and Facebook + - `credentials()` for email/password and other direct login flows + - composable `verifyCredentials()`, `startExternalAuth()`, + `finishExternalAuth()`, and `completeAuth()` primitives for session-backed + browser authentication + - auth helpers that preserve richer `fetch-router` request context types + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) diff --git a/docs/agents/remix/auth/index.md b/docs/agents/remix/auth/index.md new file mode 100644 index 0000000..c71603e --- /dev/null +++ b/docs/agents/remix/auth/index.md @@ -0,0 +1,519 @@ + + +# auth + +Composable browser authentication primitives for Remix. Use this package to +verify credentials on your own server, start external OAuth or OIDC redirects, +finish provider callbacks, and write an app-owned auth record into the session. +Pair it with +[`remix/auth-middleware`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) +when later requests need to resolve that session data into the current user and +protect routes. + +## Features + +- Small, composable primitives: `verifyCredentials()`, `startExternalAuth()`, + `finishExternalAuth()`, `refreshExternalAuth()`, and `completeAuth()` +- Built-in provider support for Google, Microsoft, Okta, Auth0, GitHub, + Facebook, X, and Atmosphere +- Module-scope provider configuration for boot-time validation and stable + callback URLs +- App-owned session records so you decide what auth data to persist +- Shared session completion for credentials and external auth flows +- Designed to pair with `remix/auth-middleware` for request-time auth resolution + and route protection + +## Installation + +```sh +npm i remix +``` + +## Usage + +`remix/auth` exposes five primitives: + +- `verifyCredentials(provider, context)` parses submitted credentials and + returns the authenticated result or `null` +- `startExternalAuth(provider, context, options?)` stores the in-progress OAuth + transaction in the session and returns the provider redirect response +- `finishExternalAuth(provider, context, options?)` validates the callback, + clears the stored transaction, and returns `{ result, returnTo? }`, including + any provider tokens in `result.tokens` +- `refreshExternalAuth(provider, tokens)` exchanges a previously stored + `refreshToken` for a fresh provider token bundle when the provider runtime + supports refresh +- `completeAuth(context)` rotates the current session id and returns the session + for auth writes + +The route owns redirects, flashes, and other app-specific behavior. `remix/auth` +owns the protocol work. + +## Credentials Auth + +Use `createCredentialsAuthProvider()` when your own server can verify submitted +credentials directly, such as email/password logins. + +```ts +import { + auth, + Auth, + createSessionAuthScheme, + requireAuth, +} from 'remix/auth-middleware' +import { + completeAuth, + createCredentialsAuthProvider, + verifyCredentials, +} from 'remix/auth' +import { createCookie } from 'remix/cookie' +import { createRouter } from 'remix/fetch-router' +import { form, route } from 'remix/fetch-router/routes' +import { FormData, formData } from 'remix/form-data-middleware' +import type { GoodAuth } from 'remix/auth-middleware' +import { redirect } from 'remix/response/redirect' +import { Session } from 'remix/session' +import { session } from 'remix/session-middleware' +import { createCookieSessionStorage } from 'remix/session/cookie-storage' + +let sessionCookie = createCookie('__session', { + secrets: [env.SESSION_SECRET], + httpOnly: true, + secure: true, + sameSite: 'lax', + path: '/', +}) + +let sessionStorage = createCookieSessionStorage() + +let routes = route({ + auth: { + session: { + login: form('/login'), + logout: { method: 'POST', pattern: '/logout' }, + }, + }, + app: { + dashboard: '/dashboard', + }, +}) + +let passwordProvider = createCredentialsAuthProvider({ + parse(context) { + let formData = context.get(FormData) + + return { + email: String(formData.get('email') ?? ''), + password: String(formData.get('password') ?? ''), + } + }, + async verify({ email, password }) { + return users.verifyPassword(email, password) + }, +}) + +let router = createRouter({ + middleware: [ + session(sessionCookie, sessionStorage), + formData(), + auth({ + schemes: [ + createSessionAuthScheme({ + read(session) { + return session.get('auth') as { userId: string } | null + }, + verify(value) { + return users.getById(value.userId) + }, + invalidate(session) { + session.unset('auth') + }, + }), + ], + }), + ], +}) + +router.get(routes.auth.session.login.index, () => new Response('Login page')) + +router.post(routes.auth.session.login.action, async (context) => { + let user = await verifyCredentials(passwordProvider, context) + + if (user == null) { + return redirect(routes.auth.session.login.index.href()) + } + + let session = completeAuth(context) + session.set('auth', { userId: user.id }) + + return redirect(routes.app.dashboard.href()) +}) + +router.post(routes.auth.session.logout, ({ get }) => { + let session = get(Session) + session.unset('auth') + session.regenerateId(true) + return redirect(routes.auth.session.login.index.href()) +}) + +router.get(routes.app.dashboard, { + middleware: [requireAuth()], + handler(context) { + let auth = context.get(Auth) as GoodAuth<{ id: string; email: string }> + + return Response.json({ + id: auth.identity.id, + email: auth.identity.email, + method: auth.method, + }) + }, +}) +``` + +## External Auth + +Starting from the same `session()`, `auth()`, and `createSessionAuthScheme()` +setup as the credentials example above, you can add a Google login flow like +this. The provider is created once at module scope, and the routes compose +`startExternalAuth()`, `finishExternalAuth()`, and `completeAuth()` directly. + +```ts +import { + auth, + Auth, + createSessionAuthScheme, + requireAuth, +} from 'remix/auth-middleware' +import { + completeAuth, + createGoogleAuthProvider, + finishExternalAuth, + refreshExternalAuth, + startExternalAuth, +} from 'remix/auth' +import { createCookie } from 'remix/cookie' +import { createRouter } from 'remix/fetch-router' +import { route } from 'remix/fetch-router/routes' +import type { GoodAuth } from 'remix/auth-middleware' +import { redirect } from 'remix/response/redirect' +import { session } from 'remix/session-middleware' +import { createCookieSessionStorage } from 'remix/session/cookie-storage' + +let sessionCookie = createCookie('__session', { + secrets: [env.SESSION_SECRET], + httpOnly: true, + secure: true, + sameSite: 'lax', + path: '/', +}) + +let sessionStorage = createCookieSessionStorage() + +let routes = route({ + auth: { + session: { + login: '/login', + }, + google: { + login: '/login/google', + callback: '/auth/google/callback', + }, + }, + app: { + dashboard: '/dashboard', + }, +}) + +let googleProvider = createGoogleAuthProvider({ + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + redirectUri: new URL(routes.auth.google.callback.href(), env.APP_ORIGIN), + authorizationParams: { + access_type: 'offline', + prompt: 'consent', + }, +}) + +let router = createRouter({ + middleware: [ + session(sessionCookie, sessionStorage), + auth({ + schemes: [ + createSessionAuthScheme({ + read(session) { + return session.get('auth') as { userId: string } | null + }, + verify(value) { + return users.getById(value.userId) + }, + invalidate(session) { + session.unset('auth') + }, + }), + ], + }), + ], +}) + +router.get(routes.auth.session.login, () => { + return new Response( + `Login with Google`, + { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }, + ) +}) + +router.get(routes.auth.google.login, (context) => + startExternalAuth(googleProvider, context, { + returnTo: context.url.searchParams.get('returnTo'), + }), +) + +router.get(routes.auth.google.callback, async (context) => { + let { result, returnTo } = await finishExternalAuth(googleProvider, context) + + let user = await users.upsertFromGoogle(result.profile) + await persistProviderTokens(user.id, result.tokens) + + let session = completeAuth(context) + session.set('auth', { userId: user.id }) + + return redirect(returnTo ?? routes.app.dashboard.href()) +}) + +async function getGoogleAccessToken(userId: string) { + let tokens = await readStoredProviderTokens(userId) + if (tokens == null) { + return null + } + + if (tokens.expiresAt != null && tokens.expiresAt.getTime() <= Date.now()) { + tokens = (await refreshExternalAuth(googleProvider, tokens)).tokens + await persistProviderTokens(userId, tokens) + } + + return tokens.accessToken +} + +router.get(routes.app.dashboard, { + middleware: [requireAuth()], + handler(context) { + let auth = context.get(Auth) as GoodAuth<{ + id: string + email: string | null + }> + + return Response.json({ + id: auth.identity.id, + email: auth.identity.email, + method: auth.method, + }) + }, +}) +``` + +A typical external auth flow looks like this: + +1. Create the provider once at module scope. For Atmosphere, call + `provider.prepare(handleOrDid)` only when starting the login flow. +2. Call `startExternalAuth()` from the login route. +3. Call `finishExternalAuth()` from the callback route. +4. Persist any provider tokens you want to reuse later. +5. Call `completeAuth(context)` and write your auth record into the returned + session. +6. On a later follow-up request, load the stored provider tokens, refresh them + with `refreshExternalAuth()` only if needed, then save the refreshed bundle + back to storage. +7. Return your own redirect or other response. + +## Built-in External Auth Providers + +When one of the built-in providers matches your auth provider, start there. +Google, Microsoft, Okta, and Auth0 use the shared OIDC runtime. GitHub, +Facebook, X, and Atmosphere use built-in custom OAuth flows. + +```ts +import { + createAuth0AuthProvider, + createAtmosphereAuthProvider, + createFacebookAuthProvider, + createGitHubAuthProvider, + createGoogleAuthProvider, + createMicrosoftAuthProvider, + createOktaAuthProvider, + createXAuthProvider, +} from 'remix/auth' + +let auth0Provider = createAuth0AuthProvider({ + domain: env.AUTH0_DOMAIN, + clientId: env.AUTH0_CLIENT_ID, + clientSecret: env.AUTH0_CLIENT_SECRET, + redirectUri: new URL('/auth/auth0/callback', env.APP_ORIGIN), +}) + +let atmosphereProvider = createAtmosphereAuthProvider({ + clientId: new URL('/oauth/client-metadata.json', env.APP_ORIGIN), + redirectUri: new URL('/auth/atmosphere/callback', env.APP_ORIGIN), + sessionSecret: env.SESSION_SECRET, +}) + +let facebookProvider = createFacebookAuthProvider({ + clientId: env.FACEBOOK_CLIENT_ID, + clientSecret: env.FACEBOOK_CLIENT_SECRET, + redirectUri: new URL('/auth/facebook/callback', env.APP_ORIGIN), +}) + +let githubProvider = createGitHubAuthProvider({ + clientId: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET, + redirectUri: new URL('/auth/github/callback', env.APP_ORIGIN), +}) + +let googleProvider = createGoogleAuthProvider({ + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + redirectUri: new URL('/auth/google/callback', env.APP_ORIGIN), +}) + +let microsoftProvider = createMicrosoftAuthProvider({ + tenant: 'organizations', + clientId: env.MICROSOFT_CLIENT_ID, + clientSecret: env.MICROSOFT_CLIENT_SECRET, + redirectUri: new URL('/auth/microsoft/callback', env.APP_ORIGIN), +}) + +let oktaProvider = createOktaAuthProvider({ + issuer: env.OKTA_ISSUER, + clientId: env.OKTA_CLIENT_ID, + clientSecret: env.OKTA_CLIENT_SECRET, + redirectUri: new URL('/auth/okta/callback', env.APP_ORIGIN), +}) + +let xProvider = createXAuthProvider({ + clientId: env.X_CLIENT_ID, + clientSecret: env.X_CLIENT_SECRET, + redirectUri: new URL('/auth/x/callback', env.APP_ORIGIN), +}) +``` + +Notes: + +- OIDC providers use discovery by default at `/.well-known/openid-configuration` +- Pass `metadata` when you want to skip discovery or `discoveryUrl` when the + metadata document lives elsewhere +- Default OIDC scopes are `openid profile email` +- `createGoogleAuthProvider()` uses the same OIDC runtime with Google's + published endpoints wired in directly, so it does not need a discovery request +- `createMicrosoftAuthProvider()` adds the `tenant` option and builds the issuer + from it +- `createOktaAuthProvider()` expects the full Okta issuer URL, usually something + like `https://example.okta.com/oauth2/default` +- `createAuth0AuthProvider()` expects your Auth0 domain and derives the issuer + URL for you +- `createAtmosphereAuthProvider()` returns a module-scope provider with + `prepare(handleOrDid)` for request-time atproto account discovery before + `startExternalAuth()` +- Atmosphere callback routes pass the module-scope provider directly to + `finishExternalAuth()`; the original handle or DID is stored in the sealed + OAuth transaction state +- `createAtmosphereAuthProvider()` requires `sessionSecret` and seals the + in-flight account, authorization server, nonce, and DPoP key state into the + existing OAuth transaction stored in your app session, so you do not need a + separate file or database store for the redirect step +- `createAtmosphereAuthProvider()` returns DPoP-bound token material in + `result.tokens`, including `accessToken`, `refreshToken`, authorization server + refresh details, and `dpop` JWK state for follow-up DPoP-signed requests +- `refreshExternalAuth()` supports built-in OIDC providers, X, and Atmosphere + when the stored token bundle includes a refresh token +- Providers only return refresh tokens when configured to request offline + access, such as `authorizationParams: { access_type: 'offline' }` for Google + or adding `offline.access` to X scopes +- Use `mapProfile()` with `createOIDCAuthProvider()` when you want + `result.profile` to have an app-specific type before it reaches your route + code + +Default scopes for OAuth providers that don't use OIDC discovery: + +- GitHub: `read:user user:email` +- Facebook: `public_profile email` +- X: `tweet.read users.read` + +Pass `scopes` if you need a different set for a provider. + +## Custom Auth Providers + +Use `createOIDCAuthProvider()` directly for custom external auth providers. This +is the extension point for providers that support OpenID Connect discovery, +authorization code flow, and a userinfo endpoint. Reach for a custom OAuth +provider implementation only when the provider does not support OIDC. + +```ts +import { + completeAuth, + createOIDCAuthProvider, + finishExternalAuth, + startExternalAuth, +} from 'remix/auth' +import { redirect } from 'remix/response/redirect' + +let companyProvider = createOIDCAuthProvider({ + name: 'company', + issuer: 'https://sso.acme.com', + clientId: 'acme-web', + clientSecret: 'acme-web-secret', + redirectUri: new URL('/auth/company/callback', 'https://app.acme.com'), + authorizationParams: { + prompt: 'login', + }, + mapProfile({ claims }) { + return { + id: claims.sub, + email: claims.email ?? null, + name: claims.name ?? claims.preferred_username ?? 'Unknown user', + } + }, +}) + +router.get('/login/company', (context) => + startExternalAuth(companyProvider, context, { + returnTo: context.url.searchParams.get('returnTo'), + }), +) + +router.get('/auth/company/callback', async (context) => { + let { result, returnTo } = await finishExternalAuth(companyProvider, context) + + let user = await users.upsertFromCompanySSO(result.profile) + let session = completeAuth(context) + session.set('auth', { userId: user.id }) + + return redirect(returnTo ?? '/dashboard') +}) +``` + +## Related Packages + +- [`auth-middleware`](https://github.com/remix-run/remix/tree/main/packages/auth-middleware) - + Request authentication and route protection helpers +- [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - + Form body parsing for `createCredentialsAuthProvider()` routes +- [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - + Request-scoped session loading and persistence +- [`session`](https://github.com/remix-run/remix/tree/main/packages/session) - + Session data model and storage backends +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + Router and middleware runtime + +## Related Work + +- [OAuth 2.0](https://oauth.net/2/) +- [RFC 7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636) +- [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) +- [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/cli/changelog.md b/docs/agents/remix/cli/changelog.md new file mode 100644 index 0000000..891f1e5 --- /dev/null +++ b/docs/agents/remix/cli/changelog.md @@ -0,0 +1,24 @@ +# `cli` CHANGELOG + +This is the changelog for +[`cli`](https://github.com/remix-run/remix/tree/main/packages/cli). It follows +[semantic versioning](https://semver.org/). + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/cli` with the public `runRemix()` API and + commands for project scaffolding, health checks and fixes, route inspection, + skills syncing, and running tests. The package requires Node.js 24.3.0 or + later and exposes the programmatic CLI API; use the `remix` package for the + user-facing `remix` executable. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`tar-parser@0.7.1`](https://github.com/remix-run/remix/releases/tag/tar-parser@0.7.1) + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + - [`test@0.2.0`](https://github.com/remix-run/remix/releases/tag/test@0.2.0) + +## Unreleased diff --git a/docs/agents/remix/cli/index.md b/docs/agents/remix/cli/index.md new file mode 100644 index 0000000..3b4556d --- /dev/null +++ b/docs/agents/remix/cli/index.md @@ -0,0 +1,85 @@ + + +# cli + +Command-line interface for creating and managing Remix projects. + +## Features + +- Create new Remix projects with `npx remix@next new` or installed `remix new` +- Print shell completion scripts with `remix completion` +- Check project environment and Remix app conventions with `remix doctor` +- Create low-risk project and controller files with `remix doctor --fix` +- Inspect the current app route tree with `remix routes` +- Sync Remix skills into `.agents/skills` with `remix skills` +- Run project tests with `remix test` +- Print the current Remix version with `remix version` +- Use the same CLI through the `remix` package or the `remix/cli` API +- Scaffold a starter app that matches the Remix project layout conventions + +## Installation + +Use `npx remix@next new ` to scaffold a new Remix app. Install +`remix` when you want the local `remix` command: + +```sh +npm i remix +``` + +## Shell completion + +Install bash completion: + +```sh +remix completion bash >> ~/.bashrc +``` + +Install zsh completion: + +```sh +remix completion zsh >> ~/.zshrc +``` + +## Usage + +Use `npx remix@next new my-remix-app` to scaffold a new Remix app. After +installing Remix, the equivalent local command is `remix new my-remix-app`. + +The rest of the CLI is available through the installed `remix` command: + +```sh +remix new my-remix-app +remix completion bash >> ~/.bashrc +remix doctor +remix doctor --fix +remix routes +remix routes --table +remix routes --table --no-headers +remix skills install +remix test +remix version +remix --no-color doctor +``` + +You can also run the CLI programmatically: + +```ts +import { runRemix } from 'remix/cli' + +await runRemix(['new', 'my-remix-app']) +await runRemix(['completion', 'bash']) +await runRemix(['doctor']) +await runRemix(['doctor', '--fix']) +await runRemix(['routes']) +await runRemix(['routes', '--table']) +await runRemix(['routes', '--table', '--no-headers']) +await runRemix(['skills', 'list']) +await runRemix(['test']) +await runRemix(['version']) +``` + +`runRemix()` returns the CLI exit code as a promise. + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/component/animate-basics.md b/docs/agents/remix/component/animate-basics.md deleted file mode 100644 index 940f85a..0000000 --- a/docs/agents/remix/component/animate-basics.md +++ /dev/null @@ -1,113 +0,0 @@ -# Animate basics - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/animate.md - -Declarative animations for element lifecycle and layout changes. The `animate` -prop handles three types of animations: - -- **Enter**: Animation played when an element mounts -- **Exit**: Animation played when an element is removed (element persists until - animation completes) -- **Layout**: FLIP animation when an element's position or size changes - -## How it works - -The `animate` prop is an intrinsic property that wraps the Web Animations API -(`element.animate()`). The reconciler handles the complexity: - -- **Enter**: Element animates from the specified keyframe(s) to its natural - styles -- **Exit**: Element animates from its current styles to the specified - keyframe(s) -- **Layout**: Element smoothly animates from old position/size to new using FLIP - technique -- **DOM persistence**: When a vnode is removed, the element stays in the DOM - until the exit animation finishes -- **Interruption**: If an animation is interrupted mid-flight, it reverses from - its current position rather than jumping to the other animation - -## Basic usage - -### Default animations - -Use `true` to enable default animations for each type: - -```tsx -
Hello
-``` - -This enables: - -- **Enter**: Fade in (150ms, ease-out) -- **Exit**: Fade out (150ms, ease-in) -- **Layout**: FLIP position/size animation (200ms, ease-out) - -Mix and match as needed: - -```tsx -
-
-
-``` - -### Single keyframe (shorthand) - -The `enter` keyframe defines the starting state - the element animates from -these values to its natural styles. The `exit` keyframe defines the ending state - -- the element animates from its current styles to these values: - -```tsx -
- Modal content -
-``` - -### Multi-step animations - -For complex sequences, provide an array of keyframes: - -```tsx -
- Toast notification -
-``` - -### Conditional animations - -Use falsy values to disable animations conditionally. This is useful for -skipping the enter animation on initial render: - -```tsx -
- Content -
-``` - -When `enter` is falsy (`false`, `null`, `undefined`), the element appears -instantly with no animation. The exit animation still plays when the element is -removed. - -## Navigation - -- [Animate patterns](./animate-patterns.md) -- [Animate layout](./animate-layout.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/animate-layout.md b/docs/agents/remix/component/animate-layout.md deleted file mode 100644 index d714c2b..0000000 --- a/docs/agents/remix/component/animate-layout.md +++ /dev/null @@ -1,172 +0,0 @@ -# Animate layout - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/animate.md - -The `layout` property enables automatic FLIP (First, Last, Invert, Play) -animations when an element's position or size changes due to layout shifts. -Instead of the element jumping to its new position, it smoothly animates there. - -## Basic usage - -Enable layout animations with `layout: true`: - -```tsx -
Animates position/size changes
-``` - -Or customize duration and easing, including springs: - -```tsx -import { spring } from '@remix-run/component' - -let custom = ( -
Ease
-) - -let springEasing = ( -
Bouncy
-) -``` - -## How it works - -Layout animations use the FLIP technique: - -1. **First**: Before any DOM changes, the element's current position is captured -2. **Last**: After DOM changes, the new position is measured -3. **Invert**: A CSS transform is applied to make the element appear at its old - position -4. **Play**: The transform animates to identity, moving the element to its new - position - -This approach is performant because it only animates `transform` (and optionally -`scale`), which are GPU-accelerated and don't trigger layout recalculations. - -## What gets animated - -Layout animations handle: - -- **Position changes**: Moving left/right/up/down via `translate3d()` -- **Size changes**: Width/height changes via `scale()` - -## Example: toggle switch - -A classic use case is animating a toggle knob when its `justify-content` -changes: - -```tsx -function FlipToggle(handle: Handle) { - let isOn = false - - return () => ( - - ) -} -``` - -When clicked, the knob smoothly slides from one side to the other instead of -jumping. - -## Example: list reordering - -Layout animations shine when reordering list items: - -```tsx -function ReorderableList(handle: Handle) { - let items = [ - { id: 'a', name: 'Apple' }, - { id: 'b', name: 'Banana' }, - { id: 'c', name: 'Cherry' }, - ] - - function shuffle() { - items = [...items].sort(() => Math.random() - 0.5) - handle.update() - } - - return () => ( - <> - -
    - {items.map((item) => ( -
  • - {item.name} -
  • - ))} -
- - ) -} -``` - -Each item animates to its new position when the list order changes. - -## Combining with enter/exit - -Layout animations work alongside enter/exit animations: - -```tsx -
- Fades in/out and animates position changes -
-``` - -## Interruption - -Layout animations are interruptible. If the layout changes again while an -animation is in progress: - -1. The current animation is cancelled -2. The element's current visual position is captured -3. A new animation starts from that position to the new target - -This ensures smooth transitions even during rapid layout changes. - -## Configuration options - -```tsx -interface LayoutAnimationConfig { - duration?: number // Animation duration in ms (default: 200) - easing?: string // CSS easing function (default: 'ease-out') -} -``` - -All options are optional - use `layout: true` for defaults, or customize: - -```tsx -// Just layout with defaults -animate={{ layout: true }} - -// Custom duration only -animate={{ layout: { duration: 300 } }} - -// Custom easing only -animate={{ layout: { easing: 'ease-in-out' } }} - -// Spring physics -animate={{ layout: { ...spring('bouncy') } }} -``` - -## Navigation - -- [Animate basics](./animate-basics.md) -- [Animate tips](./animate-tips.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/animate-patterns.md b/docs/agents/remix/component/animate-patterns.md deleted file mode 100644 index 3c009b9..0000000 --- a/docs/agents/remix/component/animate-patterns.md +++ /dev/null @@ -1,172 +0,0 @@ -# Animate patterns - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/animate.md - -## Common patterns - -### Slide down from top - -```tsx -
- Dropdown menu -
-``` - -### Slide with blur (icon swap) - -```tsx -let iconAnimation = { - enter: { - transform: 'translateY(-40px) scale(0.5)', - filter: 'blur(6px)', - duration: 100, - easing: 'ease-out', - }, - exit: { - transform: 'translateY(40px) scale(0.5)', - filter: 'blur(6px)', - duration: 100, - easing: 'ease-in', - }, -} - -// Use for swapping icons or labels - keys enable smooth cross-fade -{ - state === 'loading' ? ( -
- Loading -
- ) : ( -
- Done -
- ) -} -``` - -### Enter only (no exit animation) - -Element animates in but disappears instantly when removed: - -```tsx -
One-way animation
-``` - -### Exit only (no enter animation) - -Element appears instantly but animates out: - -```tsx -
Fade out only
-``` - -### With delay - -Stagger animations or wait before starting: - -```tsx -
Delayed entrance
-``` - -## Interruption handling - -If a user toggles an element before its animation finishes, the current -animation reverses from its current position rather than jumping to the other -animation. This creates smooth, interruptible transitions. - -```tsx -// User clicks "Toggle" to show element -// Enter animation starts: opacity 0 -> 1 -// User clicks "Toggle" again at opacity 0.4 -// Animation reverses: opacity 0.4 -> 0 (doesn't jump to exit animation) -``` - -If an exit animation is interrupted, it reverses and the node is reclaimed back -into the virtual DOM. - -**Important**: For reclamation to work, the element must have a `key` prop: - -```tsx -// Reclamation works - element can be interrupted and reused -{ - show && ( -
- ... -
- ) -} - -// No reclamation - element is recreated each time -{ - show &&
-} -``` - -Without a key, the reconciler can't determine if a new element should reclaim an -exiting one, so interrupting an exit animation will still remove the old element -and create a new one. - -## With spring easing - -Spread a spring value to get physics-based `duration` and `easing`: - -```tsx -import { spring } from '@remix-run/component' - -let el = ( -
- Bouncy modal -
-) -``` - -See [Spring basics](./spring-basics.md) for available presets and custom spring -options. - -## Complete example - -A toggle component with animate: - -```tsx -import { createRoot, type Handle } from '@remix-run/component' - -function ToggleContent(handle: Handle) { - let show = false - - return () => ( - <> - - - {show && ( -
- Content that animates in and out -
- )} - - ) -} -``` - -## Navigation - -- [Animate basics](./animate-basics.md) -- [Animate layout](./animate-layout.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/animate-tips.md b/docs/agents/remix/component/animate-tips.md deleted file mode 100644 index de327aa..0000000 --- a/docs/agents/remix/component/animate-tips.md +++ /dev/null @@ -1,40 +0,0 @@ -# Animate tips - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/animate.md - -## Tips - -- **Keep durations short**: 100-300ms feels snappy. Longer durations can feel - sluggish. -- **Use `ease-out` for enter**: Elements should decelerate as they arrive at - their final position. -- **Use `ease-in` for exit**: Elements should accelerate as they leave. -- **Use springs for layout**: Physics-based easing feels natural for - position/size changes. -- **Always use `key` for animated elements**: Keys are required for reclamation - (interrupting exit to re-enter) and for layout animations to track element - identity. Even conditionally rendered elements need keys: - `{show && }` -- **Skip animation on first render**: For elements like labels that shouldn't - animate on initial mount, use a falsy value for `enter`: - -```tsx -function Label(handle: Handle) { - let isFirstRender = true - handle.queueTask(() => { - isFirstRender = false - }) - - return (props: { text: string }) => ( - - {props.text} - - ) -} -``` - -## Navigation - -- [Animate layout](./animate-layout.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/components.md b/docs/agents/remix/component/components.md deleted file mode 100644 index e1d59c6..0000000 --- a/docs/agents/remix/component/components.md +++ /dev/null @@ -1,141 +0,0 @@ -# Components - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/components.md - -All components follow a consistent two-phase structure. - -## Component structure - -1. **Setup phase** - Runs once when the component is first created -2. **Render phase** - Runs on initial render and every update afterward - -```tsx -function MyComponent(handle: Handle, setup: SetupType) { - // Setup phase: runs once - let state = initializeState(setup) - - // Return render function: runs on every update - return (props: Props) => { - return
{/* render content */}
- } -} -``` - -## Runtime behavior - -When a component is rendered: - -1. **First render**: - -- The component function is called with `handle` and the `setup` prop -- The returned render function is stored -- The render function is called with regular props -- Any tasks queued via `handle.queueTask()` are executed after rendering - -2. **Subsequent updates**: - -- Only the render function is called -- Setup phase is skipped, setup closure persists for the lifetime of the - component instance -- Props are passed to the render function -- The `setup` prop is stripped from props -- Tasks queued during the update are executed after rendering - -3. **Component removal**: - -- `handle.signal` is aborted -- All event listeners registered via `handle.on()` are automatically cleaned up -- Any queued tasks are executed with an aborted signal - -## Setup vs props - -The `setup` prop is special - it's only available in the setup phase and is -automatically excluded from props. This prevents accidental stale captures: - -```tsx -function Counter(handle: Handle, setup: number) { - // setup prop (e.g., initialCount) only available here - let count = setup - - return (props: { label: string }) => { - // props only receives { label } - setup is excluded - return ( -
- {props.label}: {count} -
- ) - } -} - -// Usage -let element = -``` - -## Basic rendering - -The simplest component just returns JSX: - -```tsx -function Greeting() { - return (props: { name: string }) =>
Hello, {props.name}!
-} - -let el = -``` - -## Prop passing - -Props flow from parent to child through JSX attributes: - -```tsx -function Parent() { - return () => -} - -function Child() { - return (props: { message: string; count: number }) => ( -
-
{props.message}
-
Count: {props.count}
-
- ) -} -``` - -## Stateful updates - -State is managed with plain JavaScript variables. Call `handle.update()` to -trigger a re-render: - -```tsx -function Counter(handle: Handle) { - let count = 0 - - return () => ( -
-
Count: {count}
- -
- ) -} -``` - -## See also - -- [Handle API](./handle-updates.md) - Complete handle API reference -- [Patterns](./patterns-state.md) - State management best practices - -## Navigation - -- [Component index](./index.md) -- [Remix package index](../index.md) diff --git a/docs/agents/remix/component/composition-basics.md b/docs/agents/remix/component/composition-basics.md deleted file mode 100644 index 2cf7a15..0000000 --- a/docs/agents/remix/component/composition-basics.md +++ /dev/null @@ -1,121 +0,0 @@ -# Composition basics - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/composition.md - -Building component trees with props, children, and `connect`. - -## Props - -Props flow from parent to child through JSX attributes: - -```tsx -function Parent() { - return () => -} - -function Child() { - return (props: { message: string; count: number }) => ( -
-
{props.message}
-
Count: {props.count}
-
- ) -} -``` - -## Children - -Components can compose other components via `children`: - -```tsx -function Layout() { - return (props: { children: RemixNode }) => ( -
-
My App
-
{props.children}
-
(c) 2024
-
- ) -} - -function App() { - return () => ( - -

Welcome

-

Content goes here

-
- ) -} -``` - -## Connect prop - -Use the `connect` prop to get a reference to the DOM node after it's rendered. -This is useful for DOM operations like focusing elements, scrolling, measuring -dimensions, or setting up observers. - -```tsx -function Form(handle: Handle) { - let inputRef: HTMLInputElement - - return () => ( -
- (inputRef = node)} /> - -
- ) -} -``` - -The `connect` callback can optionally receive an `AbortSignal` as a second -parameter, which is aborted when the element is removed from the DOM. Use this -for cleanup operations: - -```tsx -function ResizeTracker(handle: Handle) { - let dimensions = { width: 0, height: 0 } - - return () => ( -
{ - // Set up ResizeObserver - let observer = new ResizeObserver((entries) => { - let entry = entries[0] - if (entry) { - dimensions.width = Math.round(entry.contentRect.width) - dimensions.height = Math.round(entry.contentRect.height) - handle.update() - } - }) - observer.observe(node) - - // Clean up when element is removed - signal.addEventListener('abort', () => { - observer.disconnect() - }) - }} - > - Size: {dimensions.width} x {dimensions.height} -
- ) -} -``` - -The `connect` callback is called only once when the element is first rendered, -not on every update. - -## Navigation - -- [Composition keys](./composition-keys.md) -- [Component index](./index.md) -- [Remix package index](../index.md) diff --git a/docs/agents/remix/component/composition-keys.md b/docs/agents/remix/component/composition-keys.md deleted file mode 100644 index 932df5e..0000000 --- a/docs/agents/remix/component/composition-keys.md +++ /dev/null @@ -1,99 +0,0 @@ -# Composition keys - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/composition.md - -## Key prop - -Use the `key` prop to uniquely identify elements in lists. Keys enable efficient -diffing and preserve DOM nodes and component state when lists are reordered, -filtered, or updated. - -```tsx -function TodoList(handle: Handle) { - let todos = [ - { id: '1', text: 'Buy milk' }, - { id: '2', text: 'Walk dog' }, - { id: '3', text: 'Write code' }, - ] - - return () => ( -
    - {todos.map((todo) => ( -
  • {todo.text}
  • - ))} -
- ) -} -``` - -When you reorder, add, or remove items, keys ensure: - -- **DOM nodes are reused** - Elements with matching keys are moved, not - recreated -- **Component state is preserved** - Component instances persist across reorders -- **Focus and selection are maintained** - Input focus stays with the same - element -- **Input values are preserved** - Form values remain with their elements - -```tsx -function ReorderableList(handle: Handle) { - let items = [ - { id: 'a', label: 'Item A' }, - { id: 'b', label: 'Item B' }, - { id: 'c', label: 'Item C' }, - ] - - function reverse() { - items = [...items].reverse() - handle.update() - } - - return () => ( -
- -
    - {items.map((item) => ( -
  • - -
  • - ))} -
-
- ) -} -``` - -Even when the list order changes, each input maintains its value and focus state -because the `key` prop identifies which DOM node corresponds to which item. - -Keys can be any type (string, number, bigint, object, symbol), but should be -stable and unique within the list: - -```tsx -// Good: stable, unique IDs -{ - items.map((item) =>
) -} - -// Good: index can work if list never reorders -{ - items.map((item, index) =>
) -} - -// Bad: don't use random values or values that change -{ - items.map((item) =>
) -} -``` - -## See also - -- [Context](./context.md) - Indirect composition without prop drilling -- [Animate basics](./animate-basics.md) - Keys are required for reclamation - -## Navigation - -- [Composition basics](./composition-basics.md) -- [Component index](./index.md) -- [Remix package index](../index.md) diff --git a/docs/agents/remix/component/events-basics.md b/docs/agents/remix/component/events-basics.md deleted file mode 100644 index ec8d3cd..0000000 --- a/docs/agents/remix/component/events-basics.md +++ /dev/null @@ -1,105 +0,0 @@ -# Event handling basics - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/events.md - -Event handling with the `on` prop and signal-based interruption management. - -## Basic event handling - -Use the `on` prop to attach event listeners to elements: - -```tsx -function Button(handle: Handle) { - let count = 0 - - return () => ( - - ) -} -``` - -## Event handler signature - -Event handlers receive the event object and an optional `AbortSignal`: - -```tsx -on={{ - click(event) { - // event is the DOM event - event.preventDefault() - }, - async input(event, signal) { - // signal is aborted when handler is re-entered or component removed - let response = await fetch('/api', { signal }) - } -}} -``` - -## Signals in event handlers - -Event handlers receive an `AbortSignal` that's automatically aborted when: - -- The handler is re-entered (user triggers another event before the previous one - completes) -- The component is removed from the tree - -This prevents race conditions when users create events faster than async work -completes: - -```tsx -function SearchInput(handle: Handle) { - let results: string[] = [] - let loading = false - - return () => ( -
- - {loading &&
Loading...
} - {!loading && results.length > 0 && ( -
    - {results.map((result, i) => ( -
  • {result}
  • - ))} -
- )} -
- ) -} -``` - -The signal ensures only the latest search request completes, preventing stale -results from overwriting newer ones. - -## Navigation - -- [Event patterns](./events-patterns.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/events-best-practices.md b/docs/agents/remix/component/events-best-practices.md deleted file mode 100644 index 413c51f..0000000 --- a/docs/agents/remix/component/events-best-practices.md +++ /dev/null @@ -1,96 +0,0 @@ -# Event best practices - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/events.md - -## Prefer press events over click - -For interactive elements, prefer `press` events over `click`. Press events -provide better cross-device behavior: - -- Fire on both mouse and touch interactions -- Handle keyboard activation (Enter/Space) automatically -- Prevent ghost clicks on touch devices -- Support press-and-hold patterns - -```tsx -// BAD: click doesn't handle all interaction modes well - - -// GOOD: press handles mouse, touch, and keyboard uniformly - -``` - -Use `click` only when you specifically need mouse-click behavior (e.g., -detecting right-clicks or modifier keys). - -## Do work in event handlers - -Do as much work as possible in event handlers. Use the event handler scope for -transient state: - -```tsx -// GOOD: Do work in handler, only store what renders need -function SearchResults(handle: Handle) { - let results: string[] = [] // Needed for rendering - let loading = false // Needed for rendering loading state - - return () => ( -
- - {loading &&
Loading...
} - {results.map((result, i) => ( -
{result}
- ))} -
- ) -} -``` - -## Always check `signal.aborted` - -For async work, always check the signal or pass it to APIs that support it: - -```tsx -on={{ - async click(event, signal) { - // Option 1: Pass signal to fetch - let response = await fetch('/api', { signal }) - - // Option 2: Manual check after await - let data = await someAsyncOperation() - if (signal.aborted) return - - // Safe to update state - handle.update() - } -}} -``` - -## See also - -- [Handle signals and listeners](./handle-signals.md) -- [Pattern: data loading](./patterns-data-loading.md) - -## Navigation - -- [Event patterns](./events-patterns.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/events-patterns.md b/docs/agents/remix/component/events-patterns.md deleted file mode 100644 index b576c80..0000000 --- a/docs/agents/remix/component/events-patterns.md +++ /dev/null @@ -1,164 +0,0 @@ -# Event patterns - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/events.md - -## Multiple event types - -Handle multiple events on the same element: - -```tsx -function InteractiveBox(handle: Handle) { - let state = 'idle' - - return () => ( -
- State: {state} -
- ) -} -``` - -## Form events - -Common form event patterns: - -```tsx -function Form(handle: Handle) { - return () => ( -
- - -
- ) -} -``` - -## Keyboard events - -Handle keyboard interactions: - -```tsx -function KeyboardNav(handle: Handle) { - let selectedIndex = 0 - let items = ['Apple', 'Banana', 'Cherry'] - - return () => ( -
    - {items.map((item, i) => ( -
  • - {item} -
  • - ))} -
- ) -} -``` - -## Global event listeners - -Use `handle.on()` for global event targets with automatic cleanup: - -```tsx -function WindowResizeTracker(handle: Handle) { - let width = window.innerWidth - let height = window.innerHeight - - // Set up global listeners once in setup - handle.on(window, { - resize() { - width = window.innerWidth - height = window.innerHeight - handle.update() - }, - }) - - return () => ( -
- Window size: {width} x {height} -
- ) -} -``` - -```tsx -function KeyboardTracker(handle: Handle) { - let keys: string[] = [] - - handle.on(document, { - keydown(event) { - keys.push(event.key) - handle.update() - }, - }) - - return () =>
Keys: {keys.join(', ')}
-} -``` - -## Navigation - -- [Event handling basics](./events-basics.md) -- [Event best practices](./events-best-practices.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/getting-started.md b/docs/agents/remix/component/getting-started.md deleted file mode 100644 index 79f5e62..0000000 --- a/docs/agents/remix/component/getting-started.md +++ /dev/null @@ -1,93 +0,0 @@ -# Getting started - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/getting-started.md - -Create interactive UIs with Remix Component using a two-phase component model: -setup runs once, render runs on every update. - -## Creating a root - -To start using Remix Component, create a root and render your top-level -component: - -```tsx -import { createRoot } from '@remix-run/component' -import type { Handle } from '@remix-run/component' - -function App(handle: Handle) { - return () =>
Hello, World!
-} - -// Create a root attached to a DOM element -let container = document.getElementById('app')! -let root = createRoot(container) - -// Render your app -root.render() -``` - -The `createRoot` function takes a DOM element (or `document.body`) and returns a -root object with a `render` method. You can call `render` multiple times to -update the app: - -```tsx -function App(handle: Handle) { - let count = 0 - - return () => ( -
-
Count: {count}
- -
- ) -} - -let root = createRoot(document.body) -root.render() - -// Later, you can update the app by calling render again -// root.render() -``` - -## Root methods - -The root object provides several methods: - -- **`render(node)`** - Renders a component tree into the root container -- **`flush()`** - Synchronously flushes all pending updates and tasks -- **`remove()`** - Removes the component tree and cleans up - -```tsx -let root = createRoot(document.body) - -// Render initial app -root.render() - -// Flush any pending updates synchronously -root.flush() - -// Later, remove the app -root.remove() -``` - -## Next steps - -- [Components](./components.md) - Component structure and runtime behavior -- [Handle API](./handle-updates.md) - The component's interface to the framework -- [Styling](./styling-basics.md) - CSS prop for inline styling -- [Events](./events-basics.md) - Event handling patterns - -## Navigation - -- [Component index](./index.md) -- [Remix package index](../index.md) diff --git a/docs/agents/remix/component/handle-context.md b/docs/agents/remix/component/handle-context.md deleted file mode 100644 index 72518d7..0000000 --- a/docs/agents/remix/component/handle-context.md +++ /dev/null @@ -1,41 +0,0 @@ -# Handle context - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/handle.md - -## `handle.context` - -Context API for ancestor/descendant communication. See `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 () =>
Header
-} -``` - -**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-basics.md) - Event handling patterns with signals -- [Context](./context.md) - Context API with TypedEventTarget -- [Patterns](./patterns-state.md) - Common usage patterns - -## Navigation - -- [Handle updates and tasks](./handle-updates.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/handle-signals.md b/docs/agents/remix/component/handle-signals.md deleted file mode 100644 index 57fde4f..0000000 --- a/docs/agents/remix/component/handle-signals.md +++ /dev/null @@ -1,76 +0,0 @@ -# Handle signals and listeners - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/handle.md - -## `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()}
-} -``` - -## `handle.on(target, 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[] = [] - - handle.on(document, { - keydown(event) { - keys.push(event.key) - handle.update() - }, - }) - - return () =>
Keys: {keys.join(', ')}
-} -``` - -## `handle.id` - -Stable identifier per component instance. Useful for HTML APIs like `htmlFor`, -`aria-owns`, etc. - -```tsx -function LabeledInput(handle: Handle) { - return () => ( -
- - -
- ) -} -``` - -## Navigation - -- [Handle updates and tasks](./handle-updates.md) -- [Handle context](./handle-context.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/handle-updates.md b/docs/agents/remix/component/handle-updates.md deleted file mode 100644 index bb6a605..0000000 --- a/docs/agents/remix/component/handle-updates.md +++ /dev/null @@ -1,193 +0,0 @@ -# Handle updates and tasks - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/handle.md - -The `Handle` object provides the component's interface to the framework. - -## `handle.update(task?)` - -Schedules a component update. Optionally accepts a task to run after the update -completes. - -```tsx -function Counter(handle: Handle) { - let count = 0 - - return () => ( - - ) -} -``` - -With a task: - -```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 () => ( -
- - {showDetails && ( -
(detailsSection = node)}>Details content
- )} -
- ) -} -``` - -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 - -Do not create state just to react to it in `queueTask`: - -```tsx -// BAD: Creating state just to react to it in queueTask -function BadExample(handle: Handle) { - let shouldLoad = false // Unnecessary state - - return () => ( - - ) -} - -// GOOD: Do the work directly in the event handler or queueTask -function GoodExample(handle: Handle) { - return () => ( - - ) -} -``` - -Do not call `handle.update()` before async work in a task: - -```tsx -// BAD: Calling handle.update() before async work -function BadAsyncExample(handle: Handle) { - let data: string[] = [] - let loading = false - - handle.queueTask(async (signal) => { - loading = true - handle.update() // This triggers a re-render, which aborts signal! - - let response = await fetch('/api/data', { signal }) // AbortError: signal is aborted - if (signal.aborted) return - - data = await response.json() - loading = false - handle.update() - }) - - return () =>
{loading ? 'Loading...' : data.join(', ')}
-} - -// GOOD: Set initial state in setup, only call handle.update() after async work -function GoodAsyncExample(handle: Handle) { - let data: string[] = [] - let loading = true // Start in loading state - - handle.queueTask(async (signal) => { - let response = await fetch('/api/data', { signal }) - if (signal.aborted) return - - data = await response.json() - loading = false - handle.update() // Safe - async work is complete - }) - - return () =>
{loading ? 'Loading...' : data.join(', ')}
-} -``` - -## Navigation - -- [Handle signals and listeners](./handle-signals.md) -- [Handle context](./handle-context.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/index.md b/docs/agents/remix/component/index.md deleted file mode 100644 index ac98677..0000000 --- a/docs/agents/remix/component/index.md +++ /dev/null @@ -1,52 +0,0 @@ -# component - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## README - -- [Overview, install, and quick start](./readme-overview.md) -- [Component state and setup props](./readme-components.md) -- [Events](./readme-events.md) -- [Styling and DOM connections](./readme-styling-and-connect.md) -- [Handle API reference](./readme-handle-api.md) -- [Handle listeners and signals](./readme-handle-listeners.md) -- [Handle context](./readme-handle-context.md) -- [Fragments and future work](./readme-extras.md) - -## Component docs - -- [Getting started](./getting-started.md) -- [Components](./components.md) -- [Composition basics](./composition-basics.md) -- [Composition keys](./composition-keys.md) -- [Handle updates and tasks](./handle-updates.md) -- [Handle signals and listeners](./handle-signals.md) -- [Handle context](./handle-context.md) -- [Event handling basics](./events-basics.md) -- [Event patterns](./events-patterns.md) -- [Event best practices](./events-best-practices.md) -- [Interaction basics](./interactions-basics.md) -- [Interaction examples](./interactions-examples.md) -- [Context](./context.md) -- [Styling basics](./styling-basics.md) -- [Styling selectors](./styling-selectors.md) -- [Styling with nested selectors](./styling-nesting.md) -- [Styling responsive and examples](./styling-responsive.md) -- [Pattern: state management](./patterns-state.md) -- [Pattern: setup scope](./patterns-setup.md) -- [Pattern: focus and scroll](./patterns-focus-and-scroll.md) -- [Pattern: inputs](./patterns-inputs.md) -- [Pattern: data loading](./patterns-data-loading.md) -- [Animate basics](./animate-basics.md) -- [Animate patterns](./animate-patterns.md) -- [Animate layout](./animate-layout.md) -- [Animate tips](./animate-tips.md) -- [Spring basics](./spring-basics.md) -- [Spring advanced usage](./spring-advanced.md) -- [Tween basics](./tween-basics.md) -- [Tween advanced usage](./tween-advanced.md) -- [Testing](./testing.md) - -## Navigation - -- [Remix package index](../index.md) diff --git a/docs/agents/remix/component/interactions-basics.md b/docs/agents/remix/component/interactions-basics.md deleted file mode 100644 index b5a3626..0000000 --- a/docs/agents/remix/component/interactions-basics.md +++ /dev/null @@ -1,166 +0,0 @@ -# Interaction basics - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/interactions.md - -Build reusable interaction patterns with the `@remix-run/interaction` package. - -## Built-in interactions - -The interaction package provides several ready-to-use interactions: - -```tsx -import { - press, - pressDown, - pressUp, - longPress, - pressCancel, -} from '@remix-run/interaction/press' -import { - swipeStart, - swipeMove, - swipeEnd, - swipeCancel, -} from '@remix-run/interaction/swipe' -import { - arrowUp, - arrowDown, - arrowLeft, - arrowRight, - space, -} from '@remix-run/interaction/keys' -``` - -Use them like any event type: - -```tsx - -``` - -## When to create custom interactions - -Create a custom interaction when: - -- You need to combine multiple low-level events into a semantic action -- The interaction pattern will be reused across multiple components -- You want to encapsulate complex state tracking (e.g., gesture recognition, - tempo detection) - -Do not create a custom interaction when: - -- A built-in interaction already handles your use case -- The logic is simple enough to handle inline in an event handler -- The pattern is only used in one place - -## Defining an interaction - -Use `defineInteraction` to create a reusable interaction: - -```ts -import { defineInteraction, type Interaction } from '@remix-run/interaction' - -// 1. Define the interaction with a unique namespaced type -export let dragRelease = defineInteraction('myapp:drag-release', DragRelease) - -// 2. Declare the event type for TypeScript -declare global { - interface HTMLElementEventMap { - [dragRelease]: DragReleaseEvent - } -} - -// 3. Create a custom event class with relevant data -export class DragReleaseEvent extends Event { - velocityX: number - velocityY: number - - constructor( - type: typeof dragRelease, - init: { velocityX: number; velocityY: number }, - ) { - super(type, { bubbles: true, cancelable: true }) - this.velocityX = init.velocityX - this.velocityY = init.velocityY - } -} - -// 4. Implement the interaction setup function -function DragRelease(handle: Interaction) { - if (!(handle.target instanceof HTMLElement)) return - - let target = handle.target - let isTracking = false - let velocityX = 0 - let velocityY = 0 - - handle.on(target, { - pointerdown(event) { - if (!event.isPrimary) return - isTracking = true - target.setPointerCapture(event.pointerId) - }, - - pointermove(event) { - if (!isTracking) return - // Track velocity... - }, - - pointerup(event) { - if (!isTracking) return - isTracking = false - - // Dispatch the custom event - target.dispatchEvent( - new DragReleaseEvent(dragRelease, { velocityX, velocityY }), - ) - }, - }) -} -``` - -## The interaction handle - -The setup function receives an `Interaction` handle with: - -- **`handle.target`** - The element the interaction is attached to -- **`handle.signal`** - AbortSignal for cleanup when the interaction is disposed -- **`handle.on(target, listeners)`** - Add event listeners with automatic - cleanup -- **`handle.raise(error)`** - Report errors to the parent error handler - -```ts -function MyInteraction(handle: Interaction) { - // Guard for specific element types if needed - if (!(handle.target instanceof HTMLElement)) return - - let target = handle.target - - // Set up listeners - automatically cleaned up when signal aborts - handle.on(target, { - pointerdown(event) { - // Handle event... - }, - }) - - // Listen to other targets (e.g., document for global events) - handle.on(target.ownerDocument, { - pointerup() { - // Handle pointer released outside target... - }, - }) -} -``` - -## Navigation - -- [Interaction examples](./interactions-examples.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/interactions-examples.md b/docs/agents/remix/component/interactions-examples.md deleted file mode 100644 index e196339..0000000 --- a/docs/agents/remix/component/interactions-examples.md +++ /dev/null @@ -1,113 +0,0 @@ -# Interaction examples - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/interactions.md - -## Consuming in components - -Use custom interactions just like built-in events: - -```tsx -import { dragRelease } from './drag-release.ts' - -function DraggableCard(handle: Handle) { - return () => ( -
- Drag me -
- ) -} -``` - -## Example: tap tempo - -A more complex example that tracks repeated taps to calculate BPM: - -```ts -import { defineInteraction, type Interaction } from '@remix-run/interaction' - -export let tempo = defineInteraction('myapp:tempo', Tempo) - -declare global { - interface HTMLElementEventMap { - [tempo]: TempoEvent - } -} - -export class TempoEvent extends Event { - bpm: number - - constructor(type: typeof tempo, bpm: number) { - super(type) - this.bpm = bpm - } -} - -function Tempo(handle: Interaction) { - if (!(handle.target instanceof HTMLElement)) return - - let target = handle.target - let taps: number[] = [] - let resetTimer = 0 - - function handleTap() { - clearTimeout(resetTimer) - - taps.push(Date.now()) - taps = taps.filter((tap) => Date.now() - tap < 4000) - - if (taps.length >= 4) { - let intervals = [] - for (let i = 1; i < taps.length; i++) { - intervals.push(taps[i] - taps[i - 1]) - } - let avgMs = intervals.reduce((sum, v) => sum + v, 0) / intervals.length - let bpm = Math.round(60000 / avgMs) - target.dispatchEvent(new TempoEvent(tempo, bpm)) - } - - resetTimer = window.setTimeout(() => { - taps = [] - }, 4000) - } - - handle.on(target, { - pointerdown: handleTap, - keydown(event) { - if (event.repeat) return - if (event.key === 'Enter' || event.key === ' ') { - handleTap() - } - }, - }) -} -``` - -## Best practices - -1. **Namespace your event types** - Use a prefix like `myapp:` to avoid - collisions with built-in interactions -2. **Use cancelable events** - Set `cancelable: true` so consumers can call - `event.preventDefault()` -3. **Include relevant data** - Add properties to your event class for data - consumers need -4. **Guard element types** - Check `handle.target instanceof HTMLElement` if you - need DOM-specific APIs -5. **Clean up automatically** - Use `handle.on()` instead of `addEventListener` - for automatic cleanup - -## See also - -- [Events](./events-basics.md) - Event handling basics -- [Handle updates and tasks](./handle-updates.md) - `handle.on()` in components - -## Navigation - -- [Interaction basics](./interactions-basics.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/patterns-data-loading.md b/docs/agents/remix/component/patterns-data-loading.md deleted file mode 100644 index 530a8aa..0000000 --- a/docs/agents/remix/component/patterns-data-loading.md +++ /dev/null @@ -1,127 +0,0 @@ -# Pattern: data loading - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/patterns.md - -## Using event handler signals - -Event handlers receive an `AbortSignal` that's aborted when the handler is -re-entered: - -```tsx -function SearchInput(handle: Handle) { - let results: string[] = [] - let loading = false - - return () => ( -
- - {loading &&
Loading...
} - {!loading && results.length > 0 && ( -
    - {results.map((result, i) => ( -
  • {result}
  • - ))} -
- )} -
- ) -} -``` - -## Using queueTask for reactive loading - -Use `handle.queueTask()` in the render function for reactive data loading that -responds to prop changes: - -```tsx -function DataLoader(handle: Handle) { - let data: any = null - let loading = false - let error: Error | null = null - - return (props: { url: string }) => { - // Queue data loading task that responds to prop changes - handle.queueTask(async (signal) => { - loading = true - error = null - handle.update() - - let response = await fetch(props.url, { signal }) - let json = await response.json() - if (signal.aborted) return - data = json - loading = false - handle.update() - }) - - if (loading) return
Loading...
- if (error) return
Error: {error.message}
- if (!data) return
No data
- - return
{JSON.stringify(data)}
- } -} -``` - -## Using setup scope for initial data - -Load initial data in the setup scope: - -```tsx -function UserProfile(handle: Handle, setup: { userId: string }) { - let user: User | null = null - let loading = true - - // Load initial data in setup scope using queueTask - handle.queueTask(async (signal) => { - let response = await fetch(`/api/users/${setup.userId}`, { signal }) - let data = await response.json() - if (signal.aborted) return - user = data - loading = false - handle.update() - }) - - return (props: { showEmail?: boolean }) => { - if (loading) return
Loading user...
- - return ( -
-
{user.name}
- {props.showEmail &&
{user.email}
} -
- ) - } -} -``` - -Note that by fetching this data in the setup scope any parent updates that -change `setup.userId` will have no effect. - -## See also - -- [Handle updates and tasks](./handle-updates.md) -- [Event handling basics](./events-basics.md) -- [Components](./components.md) - -## Navigation - -- [Pattern: inputs](./patterns-inputs.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/patterns-focus-and-scroll.md b/docs/agents/remix/component/patterns-focus-and-scroll.md deleted file mode 100644 index 3625727..0000000 --- a/docs/agents/remix/component/patterns-focus-and-scroll.md +++ /dev/null @@ -1,107 +0,0 @@ -# Pattern: focus and scroll - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/patterns.md - -Use `handle.queueTask()` in event handlers for DOM operations that need to -happen after the DOM has changed from the next update. - -## Focus management - -```tsx -function Modal(handle: Handle) { - let isOpen = false - let closeButton: HTMLButtonElement - let openButton: HTMLButtonElement - - return () => ( -
- - - {isOpen && ( -
- -
- )} -
- ) -} -``` - -## Scroll management - -```tsx -function ScrollableList(handle: Handle) { - let items: string[] = [] - let newItemInput: HTMLInputElement - let listContainer: HTMLElement - - return () => ( -
- (newItemInput = node)} - on={{ - keydown(event) { - if (event.key === 'Enter') { - let text = event.currentTarget.value - if (text.trim()) { - items.push(text) - event.currentTarget.value = '' - handle.update() - // Queue scroll operation after new item renders - handle.queueTask(() => { - listContainer.scrollTop = listContainer.scrollHeight - }) - } - } - }, - }} - /> -
(listContainer = node)} - css={{ - maxHeight: '300px', - overflowY: 'auto', - }} - > - {items.map((item, i) => ( -
{item}
- ))} -
-
- ) -} -``` - -## Navigation - -- [Pattern: inputs](./patterns-inputs.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/patterns-inputs.md b/docs/agents/remix/component/patterns-inputs.md deleted file mode 100644 index caab7db..0000000 --- a/docs/agents/remix/component/patterns-inputs.md +++ /dev/null @@ -1,81 +0,0 @@ -# Pattern: inputs - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/patterns.md - -Only control an input's value when something besides the user's interaction with -that input can also control its state. - -## Uncontrolled input - -Use when only the user controls the value: - -```tsx -function SearchInput(handle: Handle) { - let results: string[] = [] - - return () => ( -
- - -
- ) -} -``` - -## Controlled input - -Use when programmatic control is needed: - -```tsx -function SlugForm(handle: Handle) { - let slug = '' - let generatedSlug = '' - - return () => ( -
- - -
- ) -} -``` - -Use controlled inputs when: - -- The value can be set programmatically (auto-generated fields, reset buttons, - external state) -- The input can be disabled and its value changed by other interactions -- You need to validate or transform input before it appears -- You need to prevent certain values from being entered - -Use uncontrolled inputs when: - -- Only the user can change the value through direct interaction with that input -- You just need to read the value on events (submit, blur, etc.) - -## Navigation - -- [Pattern: data loading](./patterns-data-loading.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/patterns-setup.md b/docs/agents/remix/component/patterns-setup.md deleted file mode 100644 index b61fd0d..0000000 --- a/docs/agents/remix/component/patterns-setup.md +++ /dev/null @@ -1,148 +0,0 @@ -# Pattern: setup scope - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/patterns.md - -The setup scope is perfect for one-time initialization. - -## Initializing instances - -```tsx -function CacheExample(handle: Handle, setup: { cacheSize: number }) { - // Initialize cache once - let cache = new Map() - let maxSize = setup.cacheSize - - return (props: { key: string; value: any }) => { - // Use cache in render - if (cache.has(props.key)) { - return
Cached: {cache.get(props.key)}
- } - cache.set(props.key, props.value) - if (cache.size > maxSize) { - let firstKey = cache.keys().next().value - cache.delete(firstKey) - } - return
New: {props.value}
- } -} -``` - -## Third-party SDKs - -```tsx -function Analytics(handle: Handle, setup: { apiKey: string }) { - // Initialize SDK once - let analytics = new AnalyticsSDK(setup.apiKey) - - // Cleanup on disconnect - handle.signal.addEventListener('abort', () => { - analytics.disconnect() - }) - - return (props: { event: string; data?: any }) => { - // SDK is ready to use - return
Tracking: {props.event}
- } -} -``` - -## Event emitters - -```tsx -import { TypedEventTarget } from '@remix-run/interaction' - -class DataEvent extends Event { - constructor(public value: string) { - super('data') - } -} - -class DataEmitter extends TypedEventTarget<{ data: DataEvent }> { - emitData(value: string) { - this.dispatchEvent(new DataEvent(value)) - } -} - -function EventListener(handle: Handle, setup: DataEmitter) { - // Set up listeners once with automatic cleanup - handle.on(setup, { - data(event) { - // Handle data - handle.update() - }, - }) - - return () =>
Listening for events...
-} -``` - -## Window and document events - -```tsx -function WindowResizeTracker(handle: Handle) { - let width = window.innerWidth - let height = window.innerHeight - - // Set up global listeners once - handle.on(window, { - resize() { - width = window.innerWidth - height = window.innerHeight - handle.update() - }, - }) - - return () => ( -
- Window size: {width} x {height} -
- ) -} -``` - -## Initializing state from props - -```tsx -function Timer(handle: Handle, setup: { initialSeconds: number }) { - // Initialize from setup prop - let seconds = setup.initialSeconds - let interval: number | null = null - - function start() { - if (interval) return - interval = setInterval(() => { - seconds-- - if (seconds <= 0) { - stop() - } - handle.update() - }, 1000) - } - - function stop() { - if (interval) { - clearInterval(interval) - interval = null - } - } - - // Cleanup on disconnect - handle.signal.addEventListener('abort', stop) - - return (props: { paused?: boolean }) => { - if (!props.paused && !interval) { - start() - } else if (props.paused && interval) { - stop() - } - - return
Time remaining: {seconds}s
- } -} -``` - -## Navigation - -- [Pattern: focus and scroll](./patterns-focus-and-scroll.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/patterns-state.md b/docs/agents/remix/component/patterns-state.md deleted file mode 100644 index 27caec1..0000000 --- a/docs/agents/remix/component/patterns-state.md +++ /dev/null @@ -1,125 +0,0 @@ -# Pattern: state management - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/patterns.md - -Common patterns and best practices for building components. - -## Use minimal component state - -Only store state that's needed for rendering. Derive computed values instead of -storing them, and avoid storing input state that you don't need. - -Derive computed values: - -```tsx -// BAD: Storing computed values -function TodoList(handle: Handle) { - let todos: string[] = [] - let completedCount = 0 // Unnecessary state - - return () => ( -
- {todos.map((todo, i) => ( -
{todo}
- ))} -
Completed: {completedCount}
-
- ) -} - -// GOOD: Derive computed values in render -function TodoList(handle: Handle) { - let todos: Array<{ text: string; completed: boolean }> = [] - - return () => { - // Derive computed value in render - let completedCount = todos.filter((t) => t.completed).length - - return ( -
- {todos.map((todo, i) => ( -
{todo.text}
- ))} -
Completed: {completedCount}
-
- ) - } -} -``` - -Do not store input state you do not need: - -```tsx -// BAD: Storing input value when you only need it on submit -function SearchForm(handle: Handle) { - let query = '' // Unnecessary state - - return () => ( -
- - -
- ) -} - -// GOOD: Read input value directly from the form -function SearchForm(handle: Handle) { - return () => ( -
- - -
- ) -} -``` - -## Do work in event handlers - -Do as much work as possible in event handlers with minimal component state. Use -the event handler scope for transient event state, and only capture to component -state if it's used for rendering. - -```tsx -// GOOD: Store state that affects rendering -function Toggle(handle: Handle) { - let isOpen = false // Needed for rendering conditional content - - return () => ( -
- - {isOpen &&
Content
} -
- ) -} -``` - -## Navigation - -- [Pattern: setup scope](./patterns-setup.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-components.md b/docs/agents/remix/component/readme-components.md deleted file mode 100644 index 3cca620..0000000 --- a/docs/agents/remix/component/readme-components.md +++ /dev/null @@ -1,103 +0,0 @@ -# Component README: state and setup - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## Component state and updates - -State is managed with plain JavaScript variables. Call `handle.update()` to -schedule an update: - -```tsx -function Counter(handle: Handle) { - let count = 0 - - return () => ( -
- Count: {count} - -
- ) -} -``` - -## Components - -All components return a render function. The setup function runs once when the -component is first created, and the returned render function runs on the first -render and every update afterward: - -```tsx -function Counter(handle: Handle, setup: number) { - // Setup phase: runs once - let count = setup - - // Return render function: runs on every update - return (props: { label?: string }) => ( -
- {props.label || 'Count'}: {count} - -
- ) -} -``` - -### Setup prop vs props - -When a component returns a function, it has two phases: - -1. **Setup phase** - The component function receives the `setup` prop and runs - once. Use this for initialization. -2. **Render phase** - The returned function receives props and runs on initial - render and every update afterward. Use this for rendering. - -The `setup` prop is separate from regular props. Only the `setup` prop is passed -to the setup function, and only props are passed to the render function. - -- `setup` prop for values that initialize state (e.g., `initial`, - `defaultValue`) -- Regular props for values that change over time (e.g., `label`, `disabled`) - -```tsx -// Usage: setup prop goes to setup function, regular props go to render function -let el = - -function Counter( - handle: Handle, - setup: number, // receives 5 (the setup prop value) -) { - let count = setup // use setup for initialization - - return (props: { label?: string }) => { - // props only receives { label: "Total" } - not the setup prop - return ( -
- {props.label}: {count} -
- ) - } -} -``` - -## Navigation - -- [README overview](./readme-overview.md) -- [Events](./readme-events.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-events.md b/docs/agents/remix/component/readme-events.md deleted file mode 100644 index 8f8b054..0000000 --- a/docs/agents/remix/component/readme-events.md +++ /dev/null @@ -1,62 +0,0 @@ -# Component README: events - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## Events - -Events use the `on` prop and are handled by -[`@remix-run/interaction`](../interaction/index.md). Listeners receive an -`AbortSignal` that's aborted when the component is disconnected or the handler -is re-entered. - -```tsx -function SearchInput(handle: Handle) { - let query = '' - - return () => ( - { - query = event.currentTarget.value - handle.update() - - // Pass the signal to abort the fetch on re-entry or node removal - // This avoids race conditions in the UI and manages cleanup - fetch(`/search?q=${query}`, { signal }) - .then((res) => res.json()) - .then((results) => { - if (signal.aborted) return - // Update results - }) - }, - }} - /> - ) -} -``` - -You can also listen to global event targets like `document` or `window` using -`handle.on()` with automatic cleanup on component removal: - -```tsx -function KeyboardTracker(handle: Handle) { - let keys: string[] = [] - - handle.on(document, { - keydown: (event) => { - keys.push(event.key) - handle.update() - }, - }) - - return () =>
Keys: {keys.join(', ')}
-} -``` - -## Navigation - -- [Styling and DOM connections](./readme-styling-and-connect.md) -- [Handle API reference](./readme-handle-api.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-extras.md b/docs/agents/remix/component/readme-extras.md deleted file mode 100644 index bc7b163..0000000 --- a/docs/agents/remix/component/readme-extras.md +++ /dev/null @@ -1,40 +0,0 @@ -# Component README: extras - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## Fragments - -Use `Fragment` to group elements without adding extra DOM nodes: - -```tsx -function List(handle: Handle) { - return () => ( - <> -
  • Item 1
  • -
  • Item 2
  • -
  • Item 3
  • - - ) -} -``` - -## Wrapping components - -- use `Props<'div'>` -- use `RemixNode` not JSX.Element, etc. - -## Future - -This package is a work in progress. Future features (demo'd at Remix Jam) -include: - -- Server Rendering -- Selective Hydration -- `` for streamable, reloadable partial server UI - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Handle context](./readme-handle-context.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-handle-api.md b/docs/agents/remix/component/readme-handle-api.md deleted file mode 100644 index 2a6d500..0000000 --- a/docs/agents/remix/component/readme-handle-api.md +++ /dev/null @@ -1,145 +0,0 @@ -# Component README: handle API - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## Component Handle API - -Components receive a `Handle` as their first argument with the following API: - -- **`handle.update(task?)`** - Schedule an update. Optionally provide a task to - run after the update. -- **`handle.queueTask(task)`** - Schedule a task to run after the next update. - Useful for DOM operations that need to happen after rendering. -- **`handle.on(target, listeners)`** - Listen to an event target with automatic - cleanup when the component disconnects. -- **`handle.signal`** - An `AbortSignal` that's aborted when the component is - disconnected. Useful for cleanup. -- **`handle.id`** - Stable identifier per component instance. -- **`handle.context`** - Context API for ancestor/descendant communication. - -### `handle.update(task?)` - -Schedule an update. Optionally provide a task to run after the update completes. - -```tsx -function Counter(handle: Handle) { - let count = 0 - - return () => ( - - ) -} -``` - -You can pass a task to run after the update: - -```tsx -function Player(handle: Handle) { - let isPlaying = false - let playButton: HTMLButtonElement - let stopButton: HTMLButtonElement - - return () => ( -
    - - -
    - ) -} -``` - -### `handle.queueTask(task)` - -Schedule a task to run after the next update. Useful for DOM operations that -need to happen after rendering. - -```tsx -function Form(handle: Handle) { - let showDetails = false - let detailsSection: HTMLElement - - return () => ( -
    - - {showDetails && ( -
    (detailsSection = node)} - css={{ - marginTop: '2rem', - padding: '1rem', - border: '1px solid #ccc', - }} - > -

    Additional Details

    -

    This section appears when the checkbox is checked.

    -
    - )} -
    - ) -} -``` - -## Navigation - -- [Handle listeners and signals](./readme-handle-listeners.md) -- [Handle context](./readme-handle-context.md) -- [Fragments and future work](./readme-extras.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-handle-context.md b/docs/agents/remix/component/readme-handle-context.md deleted file mode 100644 index 32d79b1..0000000 --- a/docs/agents/remix/component/readme-handle-context.md +++ /dev/null @@ -1,101 +0,0 @@ -# Component README: handle context - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## `handle.context` - -Context API for ancestor/descendant communication. All components are potential -context providers and consumers. Use `handle.context.set()` to provide values -and `handle.context.get()` to consume them. - -```tsx -function App(handle: Handle<{ theme: string }>) { - handle.context.set({ theme: 'dark' }) - - return () => ( -
    -
    - -
    - ) -} - -function Header(handle: Handle) { - // Consume context from App - let { theme } = handle.context.get(App) - return () => ( -
    - Header -
    - ) -} -``` - -Setting context values does not automatically trigger updates. If a provider -needs to render its own context values, call `handle.update()` after setting -them. However, since providers often don't render context values themselves, -calling `update()` can cause expensive updates of the entire subtree. Instead, -make your context an -[EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) and -have consumers subscribe to changes. - -```tsx -import { TypedEventTarget } from '@remix-run/interaction' - -class Theme extends TypedEventTarget<{ change: Event }> { - #value: 'light' | 'dark' = 'light' - - get value() { - return this.#value - } - - setValue(value: string) { - this.#value = value - this.dispatchEvent(new Event('change')) - } -} - -function App(handle: Handle) { - let theme = new Theme() - handle.context.set(theme) - - return () => ( -
    - - -
    - ) -} - -function ThemedContent(handle: Handle) { - let theme = handle.context.get(App) - - // Subscribe to theme changes and update when it changes - handle.on(theme, { change: () => handle.update() }) - - return () => ( -
    - Current theme: {theme.value} -
    - ) -} -``` - -## Navigation - -- [Handle API reference](./readme-handle-api.md) -- [Fragments and future work](./readme-extras.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-handle-listeners.md b/docs/agents/remix/component/readme-handle-listeners.md deleted file mode 100644 index a8b6345..0000000 --- a/docs/agents/remix/component/readme-handle-listeners.md +++ /dev/null @@ -1,69 +0,0 @@ -# Component README: handle listeners - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## `handle.on(target, listeners)` - -Listen to an -[EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) with -automatic cleanup when the component disconnects. Ideal for listening to events -on global event targets like `document` and `window`. - -```tsx -function KeyboardTracker(handle: Handle) { - let keys: string[] = [] - - handle.on(document, { - keydown: (event) => { - keys.push(event.key) - handle.update() - }, - }) - - return () =>
    Keys: {keys.join(', ')}
    -} -``` - -The listeners are automatically removed when the component is disconnected, so -you don't need to manually clean up. - -## `handle.signal` - -An `AbortSignal` that's aborted when the component is disconnected. Useful for -cleanup operations. - -```tsx -function Clock(handle: Handle) { - let interval = setInterval(() => { - // clear the interval when the component is disconnected - if (handle.signal.aborted) { - clearInterval(interval) - return - } - handle.update() - }, 1000) - return () => {new Date().toString()} -} -``` - -## `handle.id` - -Stable identifier per component instance. Useful for HTML APIs like `htmlFor`, -`aria-owns`, etc. so consumers don't have to supply an id. - -```tsx -function LabeledInput(handle: Handle) { - return () => ( -
    - - -
    - ) -} -``` - -## Navigation - -- [Handle API reference](./readme-handle-api.md) -- [Handle context](./readme-handle-context.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-overview.md b/docs/agents/remix/component/readme-overview.md deleted file mode 100644 index a7241d9..0000000 --- a/docs/agents/remix/component/readme-overview.md +++ /dev/null @@ -1,58 +0,0 @@ -# Component README: overview - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## Overview - -A minimal component system that leans on JavaScript and DOM primitives. - -## Features - -- **JSX Runtime** - Convenient JSX syntax -- **Component State** - State managed with plain JavaScript variables -- **Manual Updates** - Explicit control over when components update via - `handle.update()` -- **Real DOM Events** - Events are real DOM events using - [`@remix-run/interaction`](../interaction/index.md) -- **Inline CSS** - CSS prop with pseudo-selectors and nested rules - -## Installation - -```sh -bun add @remix-run/component -``` - -## Getting started - -Create a root and render a component: - -```tsx -import { createRoot } from '@remix-run/component' - -function App(handle: Handle) { - let count = 0 - return () => ( - - ) -} - -createRoot(document.body).render() -``` - -Components are functions that receive a `Handle` as their first argument. They -must return a render function that receives props. - -## Navigation - -- [Component state and setup props](./readme-components.md) -- [Events](./readme-events.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/readme-styling-and-connect.md b/docs/agents/remix/component/readme-styling-and-connect.md deleted file mode 100644 index 69d8cc9..0000000 --- a/docs/agents/remix/component/readme-styling-and-connect.md +++ /dev/null @@ -1,158 +0,0 @@ -# Component README: styling and connect - -Source: https://github.com/remix-run/remix/tree/main/packages/component - -## CSS prop - -Use the `css` prop for inline styles with pseudo-selectors and nested rules: - -```tsx -function Button(handle: Handle) { - return () => ( - - ) -} -``` - -The syntax mirrors modern CSS nesting, but in object form. Use `&` to reference -the current element in pseudo-selectors, pseudo-elements, and attribute -selectors. Use class names or other selectors directly for child selectors: - -```css -.button { - color: white; - background-color: blue; - - &:hover { - background-color: darkblue; - } - - &::before { - content: ''; - position: absolute; - } - - &[aria-selected='true'] { - border: 2px solid yellow; - } - - .icon { - width: 16px; - height: 16px; - } - - @media (max-width: 768px) { - padding: 8px; - } -} -``` - -```tsx -function Button(handle: Handle) { - return () => ( - - ) -} -``` - -## Connect prop - -Use the `connect` prop to get a reference to the DOM node after it's rendered. -This is useful for DOM operations like focusing elements, scrolling, or -measuring dimensions. - -```tsx -function Form(handle: Handle) { - let inputRef: HTMLInputElement - - return () => ( -
    - (inputRef = node)} - /> - -
    - ) -} -``` - -The `connect` callback can optionally receive an `AbortSignal` as a second -parameter, which is aborted when the element is removed from the DOM: - -```tsx -function Component(handle: Handle) { - return () => ( -
    { - // Set up something that needs cleanup - let observer = new ResizeObserver(() => { - // handle resize - }) - observer.observe(node) - - // Clean up when element is removed - signal.addEventListener('abort', () => { - observer.disconnect() - }) - }} - > - Content -
    - ) -} -``` - -## Navigation - -- [Component state and setup props](./readme-components.md) -- [Handle API reference](./readme-handle-api.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/spring-advanced.md b/docs/agents/remix/component/spring-advanced.md deleted file mode 100644 index f4e3b7a..0000000 --- a/docs/agents/remix/component/spring-advanced.md +++ /dev/null @@ -1,60 +0,0 @@ -# Spring advanced usage - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/spring.md - -## Custom springs - -Customize spring parameters for finer control: - -```tsx -import { spring } from '@remix-run/component' - -let custom = spring({ - mass: 1, - stiffness: 170, - damping: 26, - velocity: 0, -}) -``` - -Available parameters: - -- `mass` - Higher values move slower -- `stiffness` - Higher values move faster -- `damping` - Higher values reduce oscillation -- `velocity` - Initial velocity - -## Using for JS animations - -You can use the spring to compute keyframes over time for JavaScript-driven -animations: - -```tsx -import { spring } from '@remix-run/component' - -let { duration, easing } = spring('bouncy') - -let keyframes = [{ transform: 'scale(0.9)' }, { transform: 'scale(1)' }] - -element.animate(keyframes, { - duration, - easing, -}) -``` - -## Reading raw values - -Use `spring()` to compute raw animation values in a render loop: - -```ts -import { spring } from '@remix-run/component' - -let springValue = spring({ mass: 1, stiffness: 170, damping: 26 }) -// springValue.duration, springValue.easing -``` - -## Navigation - -- [Spring basics](./spring-basics.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/spring-basics.md b/docs/agents/remix/component/spring-basics.md deleted file mode 100644 index fc718e1..0000000 --- a/docs/agents/remix/component/spring-basics.md +++ /dev/null @@ -1,62 +0,0 @@ -# Spring basics - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/spring.md - -The `spring` utility computes duration and easing values for natural -spring-based motion. - -## Basic usage - -```tsx -import { spring } from '@remix-run/component' - -let { duration, easing } = spring() -``` - -Use the returned values with the `animate` prop: - -```tsx -
    -``` - -## Presets - -Available presets: - -- `default` -- `fast` -- `slow` -- `gentle` -- `bouncy` - -```tsx -
    -``` - -## Using with CSS transitions - -You can also use the values with CSS transitions: - -```tsx -let { duration, easing } = spring() - -
    -``` - -## Navigation - -- [Spring advanced usage](./spring-advanced.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/styling-basics.md b/docs/agents/remix/component/styling-basics.md deleted file mode 100644 index d4c6efc..0000000 --- a/docs/agents/remix/component/styling-basics.md +++ /dev/null @@ -1,84 +0,0 @@ -# Styling basics - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/styling.md - -The `css` prop provides inline styling with support for pseudo-selectors, -pseudo-elements, attribute selectors, descendant selectors, and media queries. -It follows modern CSS nesting selector rules. - -## Basic CSS prop - -```tsx -function Button() { - return () => ( - - ) -} -``` - -## CSS prop vs style prop - -The `css` prop produces static styles that are inserted into the document as CSS -rules, while the `style` prop applies styles directly to the element. For -dynamic styles that change frequently, use the `style` prop for better -performance: - -```tsx -// BAD: Using css prop for dynamic styles -function ProgressBar(handle: Handle) { - let progress = 0 - - return () => ( -
    - {progress}% -
    - ) -} - -// GOOD: Using style prop for dynamic styles -function ProgressBar(handle: Handle) { - let progress = 0 - - return () => ( -
    - {progress}% -
    - ) -} -``` - -Use the `css` prop for: - -- Static styles that don't change -- Styles that need pseudo-selectors (`:hover`, `:focus`, etc.) -- Styles that need media queries - -Use the `style` prop for: - -- Dynamic styles that change based on state or props -- Computed values that update frequently - -## Navigation - -- [Styling selectors](./styling-selectors.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/styling-nesting.md b/docs/agents/remix/component/styling-nesting.md deleted file mode 100644 index 80a4c27..0000000 --- a/docs/agents/remix/component/styling-nesting.md +++ /dev/null @@ -1,105 +0,0 @@ -# Styling with nested selectors - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/styling.md - -## When to use nested selectors - -Use nested selectors when parent state affects children. Do not nest when you -can style the element directly. - -This is preferable to creating JavaScript state and passing it around. Instead -of managing hover/focus state in JavaScript and passing it as props, use CSS -nested selectors to let the browser handle state transitions declaratively. - -Use nested selectors when: - -1. Parent state affects children - Parent hover/focus/state changes child - styling (prefer this over JavaScript state management) -2. Styling descendant elements - Avoid duplicating styles on every child or - creating new components just for styling - -Do not nest when: - -- Styling the element's own pseudo-states (hover, focus, etc.) -- The element controls its own styling - -Example: Parent hover affects children (use nested selectors, not JavaScript -state): - -```tsx -// BAD: Managing hover state in JavaScript -function CardWithJSState(handle: Handle) { - let isHovered = false - - return (props: { children: RemixNode }) => ( -
    -
    - Title -
    - {props.children} -
    - ) -} - -// GOOD: CSS nested selectors handle state declaratively -function Card(handle: Handle) { - return (props: { children: RemixNode }) => ( -
    -
    Title
    - {props.children} -
    - ) -} -``` - -Example: Element's own hover (style directly, no nesting needed): - -```tsx -function Button() { - return () => ( - - ) -} -``` - -## Navigation - -- [Styling selectors](./styling-selectors.md) -- [Styling responsive and examples](./styling-responsive.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/styling-responsive.md b/docs/agents/remix/component/styling-responsive.md deleted file mode 100644 index fa5d6cc..0000000 --- a/docs/agents/remix/component/styling-responsive.md +++ /dev/null @@ -1,94 +0,0 @@ -# Styling responsive and examples - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/styling.md - -## Media queries - -Use `@media` for responsive design: - -```tsx -function ResponsiveGrid() { - return (props: { children: RemixNode }) => ( -
    - {props.children} -
    - ) -} -``` - -## Complete example - -Here's a comprehensive example demonstrating parent-state-affecting-children and -media queries: - -```tsx -function ProductCard() { - return (props: { title: string; price: number; image: string }) => ( -
    - {props.title} -
    -
    - {props.title} -
    -
    ${props.price}
    - -
    -
    - ) -} -``` - -This example demonstrates: - -- Parent hover affecting children: Card hover changes title color and button - background -- Styles on elements themselves: Each element has its own `css` prop -- Element's own states: Button's `:active` state styled directly on the button -- Media queries: Responsive adjustments applied directly to elements - -## See also - -- [Spring basics](./spring-basics.md) - Physics-based animation easing -- [Animate basics](./animate-basics.md) - Declarative animations - -## Navigation - -- [Styling selectors](./styling-selectors.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/styling-selectors.md b/docs/agents/remix/component/styling-selectors.md deleted file mode 100644 index 9c9019d..0000000 --- a/docs/agents/remix/component/styling-selectors.md +++ /dev/null @@ -1,112 +0,0 @@ -# Styling selectors - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/styling.md - -## Pseudo-selectors - -Use `&` to reference the current element in pseudo-selectors: - -```tsx -function Button() { - return () => ( - - ) -} -``` - -## Pseudo-elements - -Use `&::before` and `&::after` for pseudo-elements: - -```tsx -function Badge() { - return (props: { count: number }) => ( -
    - Notifications -
    - ) -} -``` - -## Attribute selectors - -Use `&[attribute]` for attribute selectors: - -```tsx -function Input() { - return (props: { required?: boolean }) => ( - - ) -} -``` - -## Descendant selectors - -Use class names or element selectors directly for descendant selectors: - -```tsx -function Card() { - return (props: { children: RemixNode }) => ( -
    -
    Title
    - {props.children} -
    - ) -} -``` - -## Navigation - -- [Styling basics](./styling-basics.md) -- [Styling with nested selectors](./styling-nesting.md) -- [Styling responsive and examples](./styling-responsive.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/testing.md b/docs/agents/remix/component/testing.md deleted file mode 100644 index c6108ba..0000000 --- a/docs/agents/remix/component/testing.md +++ /dev/null @@ -1,58 +0,0 @@ -# Testing - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/testing.md - -Testing component behavior is just JavaScript. Use your favorite test runner. - -## Example - -```tsx -import { createRoot } from '@remix-run/component' - -describe('Counter', () => { - it('increments when clicked', () => { - let container = document.createElement('div') - let root = createRoot(container) - root.render() - - let button = container.querySelector('button')! - button.click() - - expect(button.textContent).toBe('Count: 1') - }) -}) -``` - -## Testing async behavior - -Use `AbortSignal` in event handlers to ensure predictable async behavior in -tests, just like in production. - -```tsx -it('aborts stale async work', async () => { - let signal: AbortSignal | undefined - - function Search(handle: Handle) { - return () => ( - setTimeout(resolve, 10)) - if (signal?.aborted) return - }, - }} - /> - ) - } - - // render, trigger input twice quickly... -}) -``` - -## Navigation - -- [Getting started](./getting-started.md) -- [Handle updates and tasks](./handle-updates.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/tween-advanced.md b/docs/agents/remix/component/tween-advanced.md deleted file mode 100644 index 9a82ae8..0000000 --- a/docs/agents/remix/component/tween-advanced.md +++ /dev/null @@ -1,41 +0,0 @@ -# Tween advanced usage - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/tween.md - -## Custom curves - -Define a custom cubic-bezier curve: - -```tsx -import { tween } from '@remix-run/component' - -let custom = tween({ - duration: 400, - easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', -}) -``` - -## When to use tween vs spring - -Use `tween` when: - -- You want precise duration control -- You want standard easing curves -- You want predictable timing (no overshoot) - -Use `spring` when: - -- You want natural physics-based motion -- You want smooth interruption handling -- You want dynamic response based on motion values - -## See also - -- [Spring basics](./spring-basics.md) -- [Animate basics](./animate-basics.md) - -## Navigation - -- [Tween basics](./tween-basics.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/component/tween-basics.md b/docs/agents/remix/component/tween-basics.md deleted file mode 100644 index ecdcd22..0000000 --- a/docs/agents/remix/component/tween-basics.md +++ /dev/null @@ -1,38 +0,0 @@ -# Tween basics - -Source: -https://github.com/remix-run/remix/tree/main/packages/component/docs/tween.md - -The `tween` utility generates duration and easing for time-based animations. - -## Basic usage - -```tsx -import { tween } from '@remix-run/component' - -let { duration, easing } = tween() -``` - -Use with `animate`: - -```tsx -
    -``` - -## Presets - -```tsx -
    -``` - -## Navigation - -- [Tween advanced usage](./tween-advanced.md) -- [Component index](./index.md) diff --git a/docs/agents/remix/compression-middleware/changelog.md b/docs/agents/remix/compression-middleware/changelog.md new file mode 100644 index 0000000..45f8795 --- /dev/null +++ b/docs/agents/remix/compression-middleware/changelog.md @@ -0,0 +1,57 @@ +# `compression-middleware` CHANGELOG + +This is the changelog for +[`compression-middleware`](https://github.com/remix-run/remix/tree/main/packages/compression-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.1.6 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + - [`response@0.3.3`](https://github.com/remix-run/remix/releases/tag/response@0.3.3) + +## v0.1.5 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.4 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.1.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0) + - [`response@0.3.2`](https://github.com/remix-run/remix/releases/tag/response@0.3.2) + +## v0.1.2 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + +## v0.1.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.1.0 (2025-11-25) + +Initial release of this package. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/compression-middleware/README.md) +for more details. diff --git a/docs/agents/remix/compression-middleware/index.md b/docs/agents/remix/compression-middleware/index.md index ec6f70a..1925359 100644 --- a/docs/agents/remix/compression-middleware/index.md +++ b/docs/agents/remix/compression-middleware/index.md @@ -1,28 +1,31 @@ -# compression-middleware + -Source: -https://github.com/remix-run/remix/tree/main/packages/compression-middleware +# compression-middleware -## Overview +Response compression middleware for Remix. It negotiates `br`, `gzip`, and +`deflate` from `Accept-Encoding` and applies sensible defaults for when +compression is useful. -Middleware for compressing HTTP responses for use with -[`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router). +## Features -Automatically compresses responses using `gzip`, `brotli`, or `deflate` based on -the client's `Accept-Encoding` header, with intelligent defaults for media type -filtering and threshold-based compression. +- **Encoding Negotiation** - Selects the best supported encoding from + `Accept-Encoding` +- **Compression Guards** - Skips already-compressed responses and range-enabled + responses +- **Size Thresholds** - Configurable minimum response size for compression +- **MIME Filtering** - Compresses only content types likely to benefit ## Installation ```sh -bun add @remix-run/compression-middleware +npm i remix ``` ## Usage ```ts -import { createRouter } from '@remix-run/fetch-router' -import { compression } from '@remix-run/compression-middleware' +import { createRouter } from 'remix/fetch-router' +import { compression } from 'remix/compression-middleware' let router = createRouter({ middleware: [compression()], @@ -34,12 +37,165 @@ when: - The client supports compression (`Accept-Encoding` header with a supported encoding) -- The response is large enough to benefit from compression (>=1024 bytes if +- The response is large enough to benefit from compression (≥1024 bytes if `Content-Length` is present, by default) - The response hasn't already been compressed - The response doesn't advertise range support (`Accept-Ranges: bytes`) -## Navigation +### Threshold + +**Default:** `1024` (only enforced if `Content-Length` is present) + +Set the minimum response size in bytes to compress: + +```ts +import { createRouter } from 'remix/fetch-router' +import { compression } from 'remix/compression-middleware' + +let router = createRouter({ + middleware: [ + compression({ + threshold: 2048, // Only compress responses ≥2KB + }), + ], +}) +``` + +### Encodings + +**Default:** `['br', 'gzip', 'deflate']` + +Customize which compression algorithms to support: + +```ts +import { createRouter } from 'remix/fetch-router' +import { compression } from 'remix/compression-middleware' + +let router = createRouter({ + middleware: [ + compression({ + encodings: ['br', 'gzip'], // Only use Brotli and Gzip + }), + ], +}) +``` + +The `encodings` option can also be a function that receives the response: + +```ts +import { createRouter } from 'remix/fetch-router' +import { compression } from 'remix/compression-middleware' + +let router = createRouter({ + middleware: [ + compression({ + encodings: (response) => { + // Use different encodings for server-sent events + let contentType = response.headers.get('Content-Type') + return contentType?.startsWith('text/event-stream;') + ? ['gzip', 'deflate'] + : ['br', 'gzip', 'deflate'] + }, + }), + ], +}) +``` + +### Filter Media Type + +**Default:** Uses `isCompressibleMimeType()` from +[`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) + +You can customize this behavior with the `filterMediaType` option: + +```ts +import { createRouter } from 'remix/fetch-router' +import { compression } from 'remix/compression-middleware' +import { isCompressibleMimeType } from 'remix/mime' + +let router = createRouter({ + middleware: [ + compression({ + filterMediaType(mediaType) { + // Add a custom media type to the default compressible list + return ( + isCompressibleMimeType(mediaType) || + mediaType === 'application/vnd.example+data' + ) + }, + }), + ], +}) +``` + +### Compression Options + +**Default:** Uses Node.js defaults for +[zlib](https://nodejs.org/api/zlib.html#class-options) and +[Brotli](https://nodejs.org/api/zlib.html#class-brotlioptions), with automatic +flush handling for server-sent events. + +You can pass options options to the underlying Node.js `zlib` and `brotli` +compressors for fine-grained control: + +```ts +import { createRouter } from 'remix/fetch-router' +import { compression } from 'remix/compression-middleware' +import { zlib } from 'node:zlib' + +let router = createRouter({ + middleware: [ + compression({ + zlib: { + level: 6, + }, + brotli: { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 4, + }, + }, + }), + ], +}) +``` + +Like `encodings`, both `zlib` and `brotli` options can also be functions that +receive the response: + +```ts +import zlib from 'node:zlib' +import { createRouter } from 'remix/fetch-router' +import { compression } from 'remix/compression-middleware' + +let router = createRouter({ + middleware: [ + compression({ + brotli: (response) => { + let contentType = response.headers.get('Content-Type') + return { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: contentType?.startsWith( + 'text/html;', + ) + ? 4 + : 11, + }, + } + }, + }), + ], +}) +``` + +## Related Packages + +- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + Router for the web Fetch API +- [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) - + MIME type utilities +- [`@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response) - + Response helpers + +## License -- [Options and configuration](./options.md) -- [Remix package index](../index.md) +MIT diff --git a/docs/agents/remix/compression-middleware/options.md b/docs/agents/remix/compression-middleware/options.md deleted file mode 100644 index 88317d0..0000000 --- a/docs/agents/remix/compression-middleware/options.md +++ /dev/null @@ -1,167 +0,0 @@ -# Compression options - -Source: -https://github.com/remix-run/remix/tree/main/packages/compression-middleware - -## Threshold - -**Default:** `1024` (only enforced if `Content-Length` is present) - -Set the minimum response size in bytes to compress: - -```ts -import { createRouter } from '@remix-run/fetch-router' -import { compression } from '@remix-run/compression-middleware' - -let router = createRouter({ - middleware: [ - compression({ - threshold: 2048, // Only compress responses >=2KB - }), - ], -}) -``` - -## Encodings - -**Default:** `['br', 'gzip', 'deflate']` - -Customize which compression algorithms to support: - -```ts -import { createRouter } from '@remix-run/fetch-router' -import { compression } from '@remix-run/compression-middleware' - -let router = createRouter({ - middleware: [ - compression({ - encodings: ['br', 'gzip'], // Only use Brotli and Gzip - }), - ], -}) -``` - -The `encodings` option can also be a function that receives the response: - -```ts -import { createRouter } from '@remix-run/fetch-router' -import { compression } from '@remix-run/compression-middleware' - -let router = createRouter({ - middleware: [ - compression({ - encodings: (response) => { - // Use different encodings for server-sent events - let contentType = response.headers.get('Content-Type') - return contentType?.startsWith('text/event-stream;') - ? ['gzip', 'deflate'] - : ['br', 'gzip', 'deflate'] - }, - }), - ], -}) -``` - -## Filter media type - -**Default:** Uses `isCompressibleMimeType()` from -[`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) - -You can customize this behavior with the `filterMediaType` option: - -```ts -import { createRouter } from '@remix-run/fetch-router' -import { compression } from '@remix-run/compression-middleware' -import { isCompressibleMimeType } from '@remix-run/mime' - -let router = createRouter({ - middleware: [ - compression({ - filterMediaType(mediaType) { - // Add a custom media type to the default compressible list - return ( - isCompressibleMimeType(mediaType) || - mediaType === 'application/vnd.example+data' - ) - }, - }), - ], -}) -``` - -## Compression options - -**Default:** Uses Node.js defaults for -[zlib](https://nodejs.org/api/zlib.html#class-options) and -[Brotli](https://nodejs.org/api/zlib.html#class-brotlioptions), with automatic -flush handling for server-sent events. - -You can pass options options to the underlying Node.js `zlib` and `brotli` -compressors for fine-grained control: - -```ts -import { createRouter } from '@remix-run/fetch-router' -import { compression } from '@remix-run/compression-middleware' -import { zlib } from 'node:zlib' - -let router = createRouter({ - middleware: [ - compression({ - zlib: { - level: 6, - }, - brotli: { - params: { - [zlib.constants.BROTLI_PARAM_QUALITY]: 4, - }, - }, - }), - ], -}) -``` - -Like `encodings`, both `zlib` and `brotli` options can also be functions that -receive the response: - -```ts -import zlib from 'node:zlib' -import { createRouter } from '@remix-run/fetch-router' -import { compression } from '@remix-run/compression-middleware' - -let router = createRouter({ - middleware: [ - compression({ - brotli: (response) => { - let contentType = response.headers.get('Content-Type') - return { - params: { - [zlib.constants.BROTLI_PARAM_QUALITY]: contentType?.startsWith( - 'text/html;', - ) - ? 4 - : 11, - }, - } - }, - }), - ], -}) -``` - -## Related packages - -- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - Router for the web Fetch API -- [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) - - MIME type utilities -- [`@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response) - - Response helpers - -## License - -MIT - -## Navigation - -- [Compression middleware overview](./index.md) -- [Remix package index](../index.md) diff --git a/docs/agents/remix/cookie/changelog.md b/docs/agents/remix/cookie/changelog.md new file mode 100644 index 0000000..54e3b42 --- /dev/null +++ b/docs/agents/remix/cookie/changelog.md @@ -0,0 +1,67 @@ +# `cookie` CHANGELOG + +This is the changelog for +[`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie). It +follows [semantic versioning](https://semver.org/). + +## v0.5.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.5.0 (2025-11-25) + +- Add `Cookie` class. The `createCookie` function now returns an instance of the + `Cookie` class. + + ```ts + // You can now create cookies using either approach: + import { createCookie, Cookie } from '@remix-run/cookie' + + // Factory function + let cookie = createCookie('session') + + // Or use the class directly + let cookie = new Cookie('session') + ``` + +## v0.4.1 (2025-11-19) + +- Force `secure` to be `true` when `partitioned` is `true` + +## v0.4.0 (2025-11-18) + +- BREAKING CHANGE: Remove `Cookie` class, use `createCookie` instead + + ```tsx + // before + import { Cookie } from '@remix-run/cookie' + let cookie = new Cookie('session') + + // after + import { createCookie } from '@remix-run/cookie' + let cookie = createCookie('session') + ``` + +- Add `domain`, `expires`, `httpOnly`, `maxAge`, `partitioned`, `path`, + `sameSite`, and `secure` properties to `Cookie` objects + +## v0.3.0 (2025-11-08) + +- BREAKING CHANGE: Rename `cookie.isSigned` to `cookie.signed` +- Add `createCookie` function to create a new `Cookie` object +- `CookieOptions` now extends `CookieProperties` so all cookie properties may be + set in the `Cookie` constructor + +## v0.2.0 (2025-11-04) + +- Update `@remix-run/headers` peer dep to v0.15.0 + +## v0.1.0 (2025-11-04) + +This is the initial release of `@remix-run/cookie`. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/cookie/README.md) +for more details. diff --git a/docs/agents/remix/cookie.md b/docs/agents/remix/cookie/index.md similarity index 81% rename from docs/agents/remix/cookie.md rename to docs/agents/remix/cookie/index.md index 5d24113..6f324c4 100644 --- a/docs/agents/remix/cookie.md +++ b/docs/agents/remix/cookie/index.md @@ -1,19 +1,9 @@ -# cookie - -Source: https://github.com/remix-run/remix/tree/main/packages/cookie - -## README + -Simplify HTTP cookie management in JavaScript with type-safe, secure cookie -handling. `@remix-run/cookie` provides a clean, intuitive API for creating, -parsing, and serializing HTTP cookies with built-in support for signing, secret -rotation, and comprehensive cookie attribute management. +# cookie -HTTP cookies are essential for web applications, from session management and -user preferences to authentication tokens and tracking. While the standard -cookie parsing libraries provide basic functionality, they often leave complex -scenarios like secure signing, secret rotation, and type-safe value handling up -to you. +Type-safe cookie parsing and serialization for Remix. It supports secure +signing, secret rotation, and complete cookie attribute control. ## Features @@ -28,13 +18,13 @@ to you. ## Installation ```sh -bun add @remix-run/cookie +npm i remix ``` ## Usage ```tsx -import { createCookie } from '@remix-run/cookie' +import { createCookie } from 'remix/cookie' let sessionCookie = createCookie('session', { httpOnly: true, @@ -68,7 +58,7 @@ Secret rotation is also supported, so you can easily rotate in new secrets without breaking existing cookies. ```tsx -import { Cookie } from '@remix-run/cookie' +import { Cookie } from 'remix/cookie' // Start with a single secret let sessionCookie = createCookie('session', { @@ -138,7 +128,3 @@ contains only characters that are ## License See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/cop-middleware/changelog.md b/docs/agents/remix/cop-middleware/changelog.md new file mode 100644 index 0000000..b60d585 --- /dev/null +++ b/docs/agents/remix/cop-middleware/changelog.md @@ -0,0 +1,35 @@ +# `cop-middleware` CHANGELOG + +This is the changelog for +[`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.1.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.0 + +### Minor Changes + +- Add the initial release of `@remix-run/cop-middleware`. + - Expose `cop(options)` for browser-focused cross-origin protection using + `Sec-Fetch-Site` with `Origin` fallback. + - Support trusted origins, explicit insecure bypass patterns, and custom deny + handlers. + - Allow apps to layer `cop()` ahead of `session()` and `csrf()` when they want + both browser-origin filtering and token-backed CSRF protection. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.0.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/cop-middleware/index.md b/docs/agents/remix/cop-middleware/index.md new file mode 100644 index 0000000..929f0df --- /dev/null +++ b/docs/agents/remix/cop-middleware/index.md @@ -0,0 +1,144 @@ + + +# cop-middleware + +Cross-origin protection middleware for Remix. It mirrors Go's +`CrossOriginProtection` by rejecting unsafe cross-origin browser requests +without synchronizer tokens. + +## Features + +- **Browser Provenance Checks** - Uses `Sec-Fetch-Site` when present and falls + back to `Origin` +- **Trusted Origins** - Allow specific cross-origin callers by exact origin +- **Explicit Escape Hatches** - Support insecure bypass patterns for endpoints + like webhooks +- **No Session State** - Does not require synchronizer tokens or server-side + CSRF storage + +## Installation + +```sh +npm i remix +``` + +## Usage + +```ts +import { createRouter } from 'remix/fetch-router' +import { cop } from 'remix/cop-middleware' + +let router = createRouter({ + middleware: [cop()], +}) +``` + +## Behavior + +For unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`), `cop()` follows the same +broad model as Go's `CrossOriginProtection`: + +- Allow `Sec-Fetch-Site: same-origin` +- Allow `Sec-Fetch-Site: none` +- Reject other `Sec-Fetch-Site` values unless the request matches a trusted + origin or insecure bypass +- If `Sec-Fetch-Site` is missing, compare `Origin` to the request host +- If both `Sec-Fetch-Site` and `Origin` are missing, allow the request + +This middleware is intentionally tokenless. If you cannot guarantee the +deployment assumptions behind that model, prefer +[`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware). + +## Caveats + +- `cop()` is a browser-origin guard, not a universal CSRF solution. It is + designed for deployments that can rely on modern browser provenance signals + and same-origin request handling. +- If both `Sec-Fetch-Site` and `Origin` are missing on an unsafe request, + `cop()` allows the request to continue. This is intentional so older clients + and non-browser callers do not fail closed by default. +- If `Sec-Fetch-Site` is missing, `cop()` only rejects when `Origin` is present + and does not match the request host. +- If you need stronger guarantees for session-backed form workflows, mixed + deployment environments, or requests that should not fall through when browser + provenance headers are missing, use + [`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware) + or layer both middlewares together. + +## Using with csrf-middleware + +You can also layer `cop()` in front of `csrf()` when you want both browser +provenance checks and session-backed synchronizer tokens. + +```ts +import { createCookie } from 'remix/cookie' +import { createRouter } from 'remix/fetch-router' +import { createCookieSessionStorage } from 'remix/session/cookie-storage' +import { session } from 'remix/session-middleware' +import { cop } from 'remix/cop-middleware' +import { csrf } from 'remix/csrf-middleware' + +let sessionCookie = createCookie('__session', { secrets: ['secret1'] }) +let sessionStorage = createCookieSessionStorage() + +let router = createRouter({ + middleware: [cop(), session(sessionCookie, sessionStorage), csrf()], +}) +``` + +In this setup, `cop()` runs first and rejects unsafe cross-origin browser +requests early using `Sec-Fetch-Site` and `Origin`. Requests that pass `cop()` +continue into `csrf()`, which still enforces synchronizer-token validation and +origin checks for the remaining traffic. + +## Trusted Origins + +```ts +import { createRouter } from 'remix/fetch-router' +import { cop } from 'remix/cop-middleware' + +let router = createRouter({ + middleware: [ + cop({ + trustedOrigins: ['https://admin.example.com'], + }), + ], +}) +``` + +Trusted origins must be exact origin values in the form `scheme://host[:port]`. + +## Insecure Bypass Patterns + +Bypass patterns intentionally weaken protection for specific endpoints. They +support: + +- Optional method prefixes, for example `POST /webhooks/{provider}` +- Exact paths, for example `/healthz` +- Trailing-slash subtree patterns, for example `/webhooks/` +- Single-segment wildcards with `{name}` +- Tail wildcards with `{name...}` + +```ts +import { createRouter } from 'remix/fetch-router' +import { cop } from 'remix/cop-middleware' + +let router = createRouter({ + middleware: [ + cop({ + insecureBypassPatterns: ['POST /webhooks/{provider}', '/healthz'], + }), + ], +}) +``` + +## Related Packages + +- [`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware) - + Session-backed CSRF protection with synchronizer tokens +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + Router for the web Fetch API + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/cors-middleware/changelog.md b/docs/agents/remix/cors-middleware/changelog.md new file mode 100644 index 0000000..acd24a0 --- /dev/null +++ b/docs/agents/remix/cors-middleware/changelog.md @@ -0,0 +1,35 @@ +# `cors-middleware` CHANGELOG + +This is the changelog for +[`cors-middleware`](https://github.com/remix-run/remix/tree/main/packages/cors-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.1.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.0 + +### Minor Changes + +- Add the initial release of `@remix-run/cors-middleware`. + - Expose `cors(options)` for standard CORS response headers and preflight + handling in Fetch API servers. + - Support static and dynamic origin policies, credentialed requests, allowed + and exposed headers, preflight max-age, and private network preflights. + - Allow apps to either short-circuit preflight requests or continue them into + custom `OPTIONS` handlers. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.0.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/cors-middleware/index.md b/docs/agents/remix/cors-middleware/index.md new file mode 100644 index 0000000..e9f872d --- /dev/null +++ b/docs/agents/remix/cors-middleware/index.md @@ -0,0 +1,200 @@ + + +# cors-middleware + +CORS middleware for Remix. It adds standard CORS response headers to Fetch API +servers and can either short-circuit preflight requests or pass them through to +app-defined `OPTIONS` handlers. + +## Features + +- **Preflight Handling** - Automatically handles `OPTIONS` preflight requests +- **Flexible Origin Rules** - Supports static, regex, list, and function-based + origin policies +- **Credential Support** - Supports credentialed requests with spec-safe origin + reflection +- **Header Controls** - Configure allowed and exposed headers, preflight + methods, and max age +- **Private Network Support** - Optionally allow private network preflight + requests + +## Installation + +```sh +npm i remix +``` + +## Usage + +```ts +import { createRouter } from 'remix/fetch-router' +import { cors } from 'remix/cors-middleware' + +let router = createRouter({ + middleware: [ + cors({ + origin: ['https://app.example.com', 'https://admin.example.com'], + credentials: true, + exposedHeaders: ['X-Request-Id'], + }), + ], +}) + +router.get('/api/projects', () => { + return Response.json([{ id: 'p1', name: 'Remix' }], { + headers: { + 'X-Request-Id': 'req_123', + }, + }) +}) +``` + +## Origin Policies + +`origin` supports: + +- `'*'` to allow all origins +- `string` for a single exact origin +- `RegExp` for pattern-based matching +- `Array` for multiple exact and pattern matches +- `true` to reflect the request origin +- `(origin, context) => boolean | string` for dynamic policies + +### Restrict Origins + +```ts +let router = createRouter({ + middleware: [ + cors({ + origin: ['https://app.example.com', 'https://admin.example.com'], + credentials: true, + }), + ], +}) +``` + +### Dynamic Origin Policies + +```ts +let router = createRouter({ + middleware: [ + cors({ + origin(origin, context) { + if (context.url.pathname.startsWith('/public/')) { + return '*' + } + + return origin.endsWith('.trusted.example') + }, + }), + ], +}) +``` + +## Preflight Behavior + +By default, preflight requests are short-circuited with status `204`. + +```ts +let router = createRouter({ + middleware: [ + cors({ + methods: ['GET', 'POST', 'PATCH'], + allowedHeaders: ['Authorization', 'Content-Type'], + maxAge: 600, + }), + ], +}) +``` + +Use a function-based `allowedHeaders` policy when the header allowlist depends +on the request: + +```ts +let router = createRouter({ + middleware: [ + cors({ + allowedHeaders(request) { + let requestedHeaders = request.headers.get( + 'Access-Control-Request-Headers', + ) + + if (requestedHeaders?.includes('x-admin-token')) { + return ['Authorization', 'Content-Type', 'X-Admin-Token'] + } + + return ['Authorization', 'Content-Type'] + }, + }), + ], +}) +``` + +Function-based `allowedHeaders` responses vary on +`Access-Control-Request-Headers`, so caches do not reuse a preflight response +for a different requested-header set. + +Set `preflightContinue: true` to let downstream handlers process preflight +requests. Use `preflightStatusCode` when you want short-circuited preflight +responses to return a status other than `204`. + +## Private Network Preflights + +```ts +let router = createRouter({ + middleware: [ + cors({ + allowPrivateNetwork: true, + }), + ], +}) +``` + +When `allowPrivateNetwork` is enabled, the middleware adds +`Access-Control-Allow-Private-Network: true` for preflight requests that ask for +private network access. + +## Expose Response Headers + +```ts +let router = createRouter({ + middleware: [ + cors({ + exposedHeaders: ['X-Request-Id', 'X-Trace-Id'], + }), + ], +}) +``` + +## Caveats + +- CORS is primarily a browser enforcement mechanism. Disallowed non-preflight + requests still reach your handlers unless you add separate request validation. +- When `credentials: true` is used with `origin: '*'`, the middleware reflects + the request origin and adds `Vary: Origin` so the response stays cache-safe. +- When `allowedHeaders` is a function, preflight responses vary on + `Access-Control-Request-Headers` so caches do not reuse a response for a + different requested-header set. +- `preflightContinue` and `preflightStatusCode` only affect how preflight + `OPTIONS` requests are handled. They do not change actual request + authorization. + +## Related Packages + +- [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) - + Browser-origin protection middleware for unsafe cross-origin requests +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + Router for the web Fetch API +- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - + Typed HTTP header utilities + +## Related Work + +- [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) +- [Fetch Standard: CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol) +- [expressjs/cors](https://github.com/expressjs/cors) +- [rack-cors](https://github.com/cyu/rack-cors) + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/csrf-middleware/changelog.md b/docs/agents/remix/csrf-middleware/changelog.md new file mode 100644 index 0000000..a1c8d77 --- /dev/null +++ b/docs/agents/remix/csrf-middleware/changelog.md @@ -0,0 +1,35 @@ +# `csrf-middleware` CHANGELOG + +This is the changelog for +[`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.1.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.0 + +### Minor Changes + +- Add the initial release of `@remix-run/csrf-middleware`. + - Expose `csrf(options)` and `getCsrfToken(context)` for session-backed CSRF + protection in Remix apps that accept unsafe form submissions. + - Validate a per-session token together with request origin metadata, with + support for token transport in headers, form data, and query params. + - Allow apps to layer `csrf()` after `cop()` when they need stricter + token-backed protection on top of browser-origin filtering. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.0.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/csrf-middleware/index.md b/docs/agents/remix/csrf-middleware/index.md new file mode 100644 index 0000000..3f2a641 --- /dev/null +++ b/docs/agents/remix/csrf-middleware/index.md @@ -0,0 +1,128 @@ + + +# csrf-middleware + +CSRF protection middleware for Remix. It provides synchronizer-token validation +backed by session storage, plus origin checks for unsafe requests. + +## Features + +- **Session-Backed Tokens** - Creates and persists CSRF tokens in the request + session +- **Flexible Token Extraction** - Reads tokens from headers, form fields, query + params, or a custom resolver +- **Origin Validation** - Validates `Origin`/`Referer` for unsafe methods with + customizable policies +- **Configurable Enforcement** - Control safe methods, token keys, and failure + responses + +## Installation + +```sh +npm i remix +``` + +## Usage + +This middleware requires +[`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) +to run before it. + +```ts +import { createCookie } from 'remix/cookie' +import { createRouter } from 'remix/fetch-router' +import { createCookieSessionStorage } from 'remix/session/cookie-storage' +import { session } from 'remix/session-middleware' +import { csrf, getCsrfToken } from 'remix/csrf-middleware' + +let sessionCookie = createCookie('__session', { secrets: ['secret1'] }) +let sessionStorage = createCookieSessionStorage() + +let router = createRouter({ + middleware: [session(sessionCookie, sessionStorage), csrf()], +}) + +router.get('/form', (context) => { + let token = getCsrfToken(context) + + return new Response(` +
    + + +
    + `) +}) +``` + +## Token Sources + +By default, `csrf()` checks token values in this order: + +1. Request headers: `x-csrf-token`, `x-xsrf-token`, `csrf-token` +2. Form field: `_csrf` (requires `formData()` middleware to parse request + bodies) +3. Query param: `_csrf` + +You can override extraction using `value(context)`. + +Headers and form fields are the preferred transports. Query param fallback +exists for compatibility, but it is the weakest option because tokens are more +likely to be exposed in logs, history, and copied URLs. + +## Origin Validation + +For unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`), the middleware validates +request origin. + +- Default: same-origin validation when `Origin` or `Referer` is present +- Custom: provide `origin` as string, regex, array, or function +- Missing origin behavior: controlled by `allowMissingOrigin` (default `true`) + +## Caveats + +- The synchronizer token is the primary defense in `csrf()`. `Origin` and + `Referer` checks are an additional signal, not the only protection. +- By default, unsafe requests with a valid token still pass when `Origin` and + `Referer` are both missing. Set `allowMissingOrigin: false` if your deployment + wants to require provenance headers on unsafe requests. +- Query param tokens are supported for compatibility, but they should not be the + default recommendation. Prefer headers or hidden form fields when you control + the client. +- If you want to reject more unsafe requests before token validation, especially + when browser provenance headers are available, layer + [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) + in front of `csrf()`. + +## Why This Exists + +Modern browsers now provide stronger cross-origin signals like `Sec-Fetch-Site`, +and explicit `SameSite=Lax` cookies already block many CSRF attacks. We have +considered the lighter, tokenless model used by Go's `CrossOriginProtection`, +and we think it is a good fit when a deployment can make all of the guarantees +that model depends on. + +Remix cannot assume those guarantees for every app. `csrf()` still exists as the +conservative option for apps that want synchronizer tokens in addition to origin +checks, especially for session-backed HTML form workflows and mixed deployment +environments. + +If your deployment can guarantee the prerequisites for the tokenless model, this +middleware is optional. In that case, +[`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) +may be a better fit. + +## Related Packages + +- [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) - + Middleware for tokenless cross-origin protection using browser provenance + headers +- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - + Router for the web Fetch API +- [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - + Session middleware required by `csrf()` +- [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - + Needed for form body token extraction + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/data-schema.md b/docs/agents/remix/data-schema.md deleted file mode 100644 index b5dcbcd..0000000 --- a/docs/agents/remix/data-schema.md +++ /dev/null @@ -1,65 +0,0 @@ -# data-schema - -Source: https://github.com/remix-run/remix/tree/main/packages/data-schema - -## README - -`data-schema` is a tiny, standards-aligned validation and parsing library used -across Remix packages. - -- Compatible with [Standard Schema](https://standardschema.dev/) v1 -- Runtime-agnostic (browser, Node.js, Bun, Deno, Workers) -- Designed for schema-first parsing with strong TypeScript inference - -## Installation - -```sh -npm i remix -``` - -## Package exports - -- `remix/data-schema` - core schema builders and `parse`/`parseSafe` -- `remix/data-schema/checks` - reusable validation checks (`min`, `email`, etc.) -- `remix/data-schema/coerce` - coercion helpers for stringly input -- `remix/data-schema/lazy` - lazy schema support for recursive types - -## Usage - -```ts -import { object, parse, string } from 'remix/data-schema' -import { email, minLength } from 'remix/data-schema/checks' -import * as coerce from 'remix/data-schema/coerce' - -let User = object({ - id: string(), - email: string().pipe(email()), - username: string().pipe(minLength(3)), - age: coerce.number(), -}) - -let user = parse(User, { - id: 'u1', - email: 'ada@example.com', - username: 'ada', - age: '37', -}) -``` - -Use `parseSafe` when you want structured validation results instead of thrown -errors. - -## Related packages - -- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - - SQL toolkit that validates writes with `data-schema` -- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - - common pair for parsing request input before schema validation - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/data-schema/changelog.md b/docs/agents/remix/data-schema/changelog.md new file mode 100644 index 0000000..791886b --- /dev/null +++ b/docs/agents/remix/data-schema/changelog.md @@ -0,0 +1,37 @@ +# `data-schema` CHANGELOG + +This is the changelog for +[`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema). +It follows [semantic versioning](https://semver.org/). + +## v0.3.0 + +### Minor Changes + +- Add `Schema.transform()` for mapping validated schema outputs to new values + and output types. + +## v0.2.0 + +### Minor Changes + +- Add `@remix-run/data-schema/form-data` with `object`, `field`, `fields`, + `file`, and `files` helpers for validating `FormData` and `URLSearchParams` + with `parse()` and `parseSafe()`. + +### Patch Changes + +- Remove unnecessary `as const` from `enum_()` examples in docs and tests since + the `const` type parameter already preserves literal type inference. + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/data-schema`. + +## v0.1.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/data-schema/index.md b/docs/agents/remix/data-schema/index.md new file mode 100644 index 0000000..8e1d5c5 --- /dev/null +++ b/docs/agents/remix/data-schema/index.md @@ -0,0 +1,509 @@ + + +# data-schema + +Tiny, standards-aligned data validation for Remix and the wider TypeScript +ecosystem. + +- [Standard Schema](https://standardschema.dev/) v1 compatible +- Sync-first, minimal API surface +- Runs anywhere JavaScript runs (browser, Node.js, Bun, Deno, Workers) + +## Quick start + +```ts +import { + enum_, + literal, + number, + object, + parse, + string, + variant, +} from '@remix-run/data-schema' +import { email, maxLength, min, minLength } from '@remix-run/data-schema/checks' +import * as coerce from '@remix-run/data-schema/coerce' + +let User = object({ + id: string(), + email: string().pipe(email()), + username: string().pipe(minLength(3), maxLength(20)), + age: coerce.number().pipe(min(13)), + role: enum_(['admin', 'member', 'guest']), + flags: object({ + beta: coerce.boolean(), + }), +}) + +let Event = variant('type', { + created: object({ type: literal('created'), id: string() }), + updated: object({ + type: literal('updated'), + id: string(), + version: number(), + }), +}) + +let user = parse(User, { + id: 'u1', + email: 'ada@example.com', + username: 'ada', + age: '37', + role: 'admin', + flags: { beta: 'true' }, +}) + +let event = parse(Event, { type: 'created', id: 'evt_1' }) +``` + +## Parsing + +Use `parse()` when you want a typed value or an exception. + +```ts +import { object, string, number, parse } from '@remix-run/data-schema' + +let User = object({ name: string(), age: number() }) + +let user = parse(User, { name: 'Ada', age: 37 }) +``` + +Use `parseSafe()` when you prefer explicit branching over exceptions. + +```ts +import { object, string, number, parseSafe } from '@remix-run/data-schema' + +let User = object({ name: string(), age: number() }) + +let result = parseSafe(User, input) + +if (!result.success) { + // result.issues — array of { message, path? } +} else { + let user = result.value +} +``` + +Both `parse` and `parseSafe` accept any +[Standard Schema](https://standardschema.dev/) v1 schema, not just data-schema's +own schemas. You can pass a Zod, Valibot, or ArkType schema and they'll work. + +For `FormData` and `URLSearchParams`, use the `remix/data-schema/form-data` +helpers to build schemas that plug into the same `parse()` / `parseSafe()` flow: + +```ts +import * as s from 'remix/data-schema' +import * as f from 'remix/data-schema/form-data' +import * as checks from 'remix/data-schema/checks' +import * as coerce from 'remix/data-schema/coerce' + +let Login = f.object({ + email: f.field(coerce.string().pipe(checks.email())), + password: f.field(s.string().pipe(checks.minLength(8))), +}) + +let credentials = s.parse(Login, await request.formData()) +let filters = s.parse( + f.object({ + query: f.field(s.defaulted(s.string(), '')), + tags: f.fields(s.array(s.string())), + }), + new URL(request.url).searchParams, +) +``` + +`f.object(...)` is the root schema for `FormData` and `URLSearchParams`. Use +`f.field(...)` for one text value, `f.fields(...)` for repeated text values, +`f.file(...)` for one uploaded file, and `f.files(...)` for repeated files. When +you want a fallback value, prefer `s.defaulted(s.string(), '')`. File helpers +are intended for `FormData`; `URLSearchParams` only supports text values. + +You can also customize built-in validation messages with `errorMap`: + +```ts +import { object, parseSafe, string } from '@remix-run/data-schema' +import { minLength } from '@remix-run/data-schema/checks' + +let User = object({ + name: string(), + username: string().pipe(minLength(3)), +}) +let result = parseSafe(User, input, { + locale: 'es', + errorMap(context) { + if (context.code === 'type.string') { + return 'Se esperaba texto' + } + + if (context.code === 'string.min_length') { + return ( + 'Debe tener al menos ' + + String((context.values as { min: number }).min) + + ' caracteres' + ) + } + }, +}) +``` + +`errorMap` receives `{ code, defaultMessage, path, values, input, locale }`. +Return `undefined` to keep the default message. + +## Primitives + +```ts +import { + string, + number, + boolean, + bigint, + symbol, + null_, + undefined_, +} from '@remix-run/data-schema' + +string() // validates typeof === 'string' +number() // validates finite numbers (rejects NaN, Infinity) +boolean() // validates typeof === 'boolean' +bigint() // validates typeof === 'bigint' +symbol() // validates typeof === 'symbol' +null_() // validates value === null +undefined_() // validates value === undefined +``` + +## Literals, enums, and unions + +```ts +import { literal, enum_, union } from '@remix-run/data-schema' + +// Exact value match +let yes = literal('yes') + +// One of several allowed values +let Status = enum_(['active', 'inactive', 'pending']) + +// First schema that matches wins +let StringOrNumber = union([string(), number()]) +``` + +## Objects + +```ts +import { + object, + string, + number, + optional, + defaulted, +} from '@remix-run/data-schema' + +let User = object({ + name: string(), + bio: optional(string()), // accepts undefined + role: defaulted(string(), 'user'), // fills in 'user' when undefined + age: number(), +}) +``` + +Unknown keys are stripped by default. Change this with `unknownKeys`: + +```ts +object({ name: string() }, { unknownKeys: 'passthrough' }) // keeps unknown keys +object({ name: string() }, { unknownKeys: 'error' }) // rejects unknown keys +``` + +## Collections + +```ts +import { + array, + tuple, + record, + map, + set, + string, + number, + boolean, +} from '@remix-run/data-schema' + +array(number()) // number[] +tuple([string(), number(), boolean()]) // [string, number, boolean] +record(string(), number()) // Record +map(string(), number()) // Map +set(number()) // Set +``` + +## Modifiers + +```ts +import { + nullable, + optional, + defaulted, + string, + number, +} from '@remix-run/data-schema' + +nullable(string()) // string | null +optional(number()) // number | undefined +defaulted(string(), 'n/a') // fills 'n/a' when undefined +``` + +## Instance checks + +```ts +import { instanceof_, object } from '@remix-run/data-schema' + +let Schema = object({ + created: instanceof_(Date), + pattern: instanceof_(RegExp), +}) +``` + +## Any + +Accept any value without validation. Useful when part of a structure is opaque. + +```ts +import { any, object, string } from '@remix-run/data-schema' + +let Envelope = object({ + type: string(), + payload: any(), +}) +``` + +## Custom rules with `.refine()` + +Add domain-specific validation logic inline. The predicate runs after the schema +validates. + +```ts +import { number, string, object } from '@remix-run/data-schema' + +let Profile = object({ + username: string().refine((s) => s.length >= 3, 'Too short'), + age: number().refine((n) => n >= 18, 'Must be an adult'), +}) +``` + +## Output transforms with `.transform()` + +Map a validated value into the shape your app wants. The transformer runs after +the schema validates and changes the parsed output type. + +```ts +import { object, parse, string } from '@remix-run/data-schema' + +let Event = object({ + id: string(), + createdAt: string() + .refine((value) => !Number.isNaN(Date.parse(value)), 'Expected valid date') + .transform((value) => new Date(value)), +}) + +let event = parse(Event, { + id: 'evt_1', + createdAt: '2026-04-25T00:00:00.000Z', +}) + +event.createdAt // Date +``` + +Use `.refine()` for checks that reject values without changing them. Use +`.transform()` for safe, synchronous mappings; thrown errors are propagated +instead of converted into validation issues. + +## Validation pipelines with `.pipe()` + +Compose reusable `Check` objects for common constraints. + +```ts +import { object, string, number } from '@remix-run/data-schema' +import { + minLength, + maxLength, + email, + min, + max, +} from '@remix-run/data-schema/checks' + +let Credentials = object({ + username: string().pipe(minLength(3), maxLength(20)), + email: string().pipe(email()), + age: number().pipe(min(13), max(130)), +}) +``` + +Built-in checks: `minLength`, `maxLength`, `email`, `url`, `min`, `max`. + +## Coercing input values + +Turn stringly-typed inputs (like form data or query strings) into real types at +the schema boundary. + +```ts +import { object, parse } from '@remix-run/data-schema' +import * as coerce from '@remix-run/data-schema/coerce' + +let Query = object({ + page: coerce.number(), + includeArchived: coerce.boolean(), + since: coerce.date(), + limit: coerce.bigint(), + search: coerce.string(), +}) + +let query = parse(Query, { + page: '2', + includeArchived: 'true', + since: '2025-01-01', + limit: '100', + search: 42, +}) +``` + +## Discriminated unions + +Pick the right schema based on a discriminator property. + +```ts +import { + literal, + number, + object, + string, + variant, +} from '@remix-run/data-schema' + +let Event = variant('type', { + created: object({ type: literal('created'), id: string() }), + updated: object({ + type: literal('updated'), + id: string(), + version: number(), + }), +}) +``` + +## Recursive schemas + +Model trees and self-referencing structures. `lazy()` defers schema resolution +to avoid circular references. + +```ts +import { array, object, string } from '@remix-run/data-schema' +import { lazy } from '@remix-run/data-schema/lazy' +import type { Schema } from '@remix-run/data-schema' + +type TreeNode = { id: string; children: TreeNode[] } + +let Node: Schema = lazy(() => + object({ id: string(), children: array(Node) }), +) +``` + +## Aborting early + +By default, validation collects all issues in a single pass. To stop at the +first issue, enable `abortEarly`. + +```ts +import { object, string, number, parseSafe } from '@remix-run/data-schema' + +let result = parseSafe( + object({ name: string(), age: number() }), + { name: 123, age: 'x' }, + { abortEarly: true }, +) + +if (!result.success) { + console.log(result.issues) // only the first issue +} +``` + +## Type inference + +Extract input and output types from any Standard Schema-compatible schema. + +```ts +import { object, string, number } from '@remix-run/data-schema' +import type { InferInput, InferOutput } from '@remix-run/data-schema' + +let User = object({ name: string(), age: number() }) + +type UserInput = InferInput // unknown +type UserOutput = InferOutput // { name: string; age: number } +``` + +## Extending data-schema + +Build custom schemas using `createSchema`, `createIssue`, and `fail`. These are +the same primitives used internally by every built-in schema. + +```ts +import { createSchema, createIssue, fail } from '@remix-run/data-schema' +import type { Schema } from '@remix-run/data-schema' + +// A schema that validates a non-empty trimmed string +function trimmedString(): Schema { + return createSchema(function validate(value, context) { + if (typeof value !== 'string') { + return fail('Expected string', context.path) + } + + let trimmed = value.trim() + + if (trimmed.length === 0) { + return fail('Expected non-empty string', context.path) + } + + return { value: trimmed } + }) +} + +// A schema that validates a [lat, lng] coordinate pair +function latLng(): Schema { + return createSchema(function validate(value, context) { + if (!Array.isArray(value) || value.length !== 2) { + return fail('Expected [lat, lng] pair', context.path) + } + + let issues = [] + let [lat, lng] = value + + if (typeof lat !== 'number' || lat < -90 || lat > 90) { + issues.push( + createIssue('Latitude must be between -90 and 90', [ + ...context.path, + 0, + ]), + ) + } + + if (typeof lng !== 'number' || lng < -180 || lng > 180) { + issues.push( + createIssue('Longitude must be between -180 and 180', [ + ...context.path, + 1, + ]), + ) + } + + if (issues.length > 0) { + return { issues } + } + + return { value: [lat, lng] } + }) +} +``` + +The validator function receives the raw value and a context with the current +`path` and `options`. Return `{ value }` on success or `{ issues: [...] }` on +failure. The returned schema is fully Standard Schema v1-compatible and supports +`.pipe()` and `.refine()` out of the box. + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/data-table-mysql.md b/docs/agents/remix/data-table-mysql.md deleted file mode 100644 index 47638cf..0000000 --- a/docs/agents/remix/data-table-mysql.md +++ /dev/null @@ -1,47 +0,0 @@ -# data-table-mysql - -Source: https://github.com/remix-run/remix/tree/main/packages/data-table-mysql - -## README - -MySQL adapter for `remix/data-table`. - -## Installation - -```sh -npm i remix mysql2 -``` - -## Usage - -```ts -import { createPool } from 'mysql2/promise' -import { createDatabase } from 'remix/data-table' -import { createMysqlDatabaseAdapter } from 'remix/data-table-mysql' - -let pool = createPool(process.env.DATABASE_URL as string) -let db = createDatabase(createMysqlDatabaseAdapter(pool)) -``` - -## Default capabilities - -- `returning: false` -- `savepoints: true` -- `upsert: true` - -MySQL has no native SQL `RETURNING`; write operations should use write metadata -(`affectedRows`, `insertId`) instead of returned row sets. - -## Related packages - -- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) -- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) -- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/data-table-mysql/changelog.md b/docs/agents/remix/data-table-mysql/changelog.md new file mode 100644 index 0000000..6fac2bf --- /dev/null +++ b/docs/agents/remix/data-table-mysql/changelog.md @@ -0,0 +1,73 @@ +# `data-table-mysql` CHANGELOG + +This is the changelog for +[`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql). +It follows [semantic versioning](https://semver.org/). + +## v0.3.0 + +### Minor Changes + +- BREAKING CHANGE: Removed adapter options + + **Affected APIs** + - `MysqlDatabaseAdapterOptions` type: removed + - `createMysqlDatabaseAdapter` function: `options` arg removed + - `MysqlDatabaseAdapter` constructor: `options` arg removed + + **Why** + + Adapter options existed solely for tests to override adapter capabilities. If + you must override capabilities, you can do so directly via mutation: + + ```ts + let adapter = createMysqlDatabaseAdapter(mysql) + adapter.capabilities = { + ...adapter.capabilities, + upsert: false, + } + ``` + +## v0.2.0 + +### Minor Changes + +- Add first-class migration execution support to the mysql adapter. It now + compiles and executes `DataMigrationOperation` plans for + `remix/data-table/migrations`, including create/alter/drop table and index + flows, migration journal writes, and adapter-managed migration locking. + + Normal reads/writes continue through `execute(...)`, while migration/DDL work + runs through `migrate(...)`. + + SQL compilation remains adapter-owned and can share helpers from + `remix/data-table/sql-helpers`. + +- Add transaction-aware migration introspection to the mysql adapter. + + `hasTable(table, transaction?)` and `hasColumn(table, column, transaction?)` + now use the provided migration transaction connection when present, so + planning and execution can inspect schema state inside the active migration + transaction. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`data-table@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.2.0) + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/data-table-mysql`. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0) + +## v0.1.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/data-table-mysql/index.md b/docs/agents/remix/data-table-mysql/index.md new file mode 100644 index 0000000..47a4fe4 --- /dev/null +++ b/docs/agents/remix/data-table-mysql/index.md @@ -0,0 +1,93 @@ + + +# data-table-mysql + +MySQL adapter for +[`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table). +Use this package when you want `data-table` APIs backed by `mysql2`. + +## Features + +- **Native `mysql2` Integration**: Works with `mysql2/promise` `Pool` and + `PoolConnection` instances +- **Full `data-table` API Support**: Queries, relations, writes, and + transactions +- **Adapter-Owned Compiler**: SQL compilation lives in this adapter, with + optional shared pure helpers from `data-table` +- **Migration DDL Support**: Compiles and executes `DataMigrationOperation` + operations for `remix/data-table/migrations` +- **MySQL Capabilities Enabled By Default**: + - `returning: false` + - `savepoints: true` + - `upsert: true` + - `transactionalDdl: false` + - `migrationLock: true` + +## Installation + +```sh +npm i remix mysql2 +``` + +## Usage + +```ts +import { createPool } from 'mysql2/promise' +import { createDatabase } from 'remix/data-table' +import { createMysqlDatabaseAdapter } from 'remix/data-table-mysql' + +let pool = createPool(process.env.DATABASE_URL as string) +let db = createDatabase(createMysqlDatabaseAdapter(pool)) +``` + +Use `db.query(...)`, relation loading, and transactions from `remix/data-table`. +Import any driver-specific types you need directly from `mysql2/promise`. + +## Adapter Capabilities + +`data-table-mysql` reports this capability set by default: + +- `returning: false` +- `savepoints: true` +- `upsert: true` +- `transactionalDdl: false` +- `migrationLock: true` + +## Advanced Usage + +### `returning` On MySQL + +MySQL does not natively support SQL `RETURNING`. In this adapter, using +`returning` on write operations throws `DataTableQueryError`. + +Use write metadata (`affectedRows`, `insertId`) on MySQL, or switch adapters +when returned rows are required. + +```ts +import { DataTableQueryError } from 'remix/data-table' + +try { + await db + .query(Accounts) + .insert({ email: 'a@example.com', status: 'active' }, { returning: ['id'] }) +} catch (error) { + if (error instanceof DataTableQueryError) { + // insert() returning is not supported by this adapter + } +} +``` + +## Related Packages + +- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - + Core query/relations API +- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - + Schema parsing and validation +- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - + PostgreSQL adapter +- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - + SQLite adapter + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/data-table-postgres.md b/docs/agents/remix/data-table-postgres.md deleted file mode 100644 index 8dfc8e7..0000000 --- a/docs/agents/remix/data-table-postgres.md +++ /dev/null @@ -1,48 +0,0 @@ -# data-table-postgres - -Source: -https://github.com/remix-run/remix/tree/main/packages/data-table-postgres - -## README - -PostgreSQL adapter for `remix/data-table`. - -## Installation - -```sh -npm i remix pg -``` - -## Usage - -```ts -import { Pool } from 'pg' -import { createDatabase } from 'remix/data-table' -import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres' - -let pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}) - -let db = createDatabase(createPostgresDatabaseAdapter(pool)) -``` - -## Default capabilities - -- `returning: true` -- `savepoints: true` -- `upsert: true` - -## Related packages - -- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) -- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) -- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/data-table-postgres/changelog.md b/docs/agents/remix/data-table-postgres/changelog.md new file mode 100644 index 0000000..beec0c3 --- /dev/null +++ b/docs/agents/remix/data-table-postgres/changelog.md @@ -0,0 +1,79 @@ +# `data-table-postgres` CHANGELOG + +This is the changelog for +[`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres). +It follows [semantic versioning](https://semver.org/). + +## v0.3.0 + +### Minor Changes + +- BREAKING CHANGE: Removed adapter options + + **Affected APIs** + - `PostgresDatabaseAdapterOptions` type: removed + - `createPostgresDatabaseAdapter` function: `options` arg removed + - `PostgresDatabaseAdapter` constructor: `options` arg removed + + **Why** + + Adapter options existed solely for tests to override adapter capabilities. If + you must override capabilities, you can do so directly via mutation: + + ```ts + let adapter = createPostgresDatabaseAdapter(postgres) + adapter.capabilities = { + ...adapter.capabilities, + returning: false, + } + ``` + +- Types for `createPostgresDatabaseAdapter` now accept a `Client` in addition to + `Pool` and `PoolClient`. + + This is a type-only change that aligns the function signature with existing + runtime behavior. + +## v0.2.0 + +### Minor Changes + +- Add first-class migration execution support to the postgres adapter. It now + compiles and executes `DataMigrationOperation` plans for + `remix/data-table/migrations`, including create/alter/drop table and index + flows, migration journal writes, and adapter-managed migration locking. + + Normal reads/writes continue through `execute(...)`, while migration/DDL work + runs through `migrate(...)`. + + SQL compilation remains adapter-owned and can share helpers from + `remix/data-table/sql-helpers`. + +- Add transaction-aware migration introspection to the postgres adapter. + + `hasTable(table, transaction?)` and `hasColumn(table, column, transaction?)` + now use the provided migration transaction client when present, so planning + and execution can inspect schema state inside the active migration + transaction. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`data-table@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.2.0) + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/data-table-postgres`. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0) + +## v0.1.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/data-table-postgres/index.md b/docs/agents/remix/data-table-postgres/index.md new file mode 100644 index 0000000..141b07d --- /dev/null +++ b/docs/agents/remix/data-table-postgres/index.md @@ -0,0 +1,84 @@ + + +# data-table-postgres + +PostgreSQL adapter for +[`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table). +Use this package when you want `data-table` APIs backed by `pg`. + +## Features + +- **Native `pg` Integration**: Works with `pg` `Pool` and `PoolClient` instances +- **Full `data-table` API Support**: Queries, relations, writes, and + transactions +- **Adapter-Owned Compiler**: SQL compilation lives in this adapter, with + optional shared pure helpers from `data-table` +- **Migration DDL Support**: Compiles and executes `DataMigrationOperation` + operations for `remix/data-table/migrations` +- **Postgres Capabilities Enabled By Default**: + - `returning: true` + - `savepoints: true` + - `upsert: true` + - `transactionalDdl: true` + - `migrationLock: true` + +## Installation + +```sh +npm i remix pg +``` + +## Usage + +```ts +import { Pool } from 'pg' +import { createDatabase } from 'remix/data-table' +import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres' + +let pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}) + +let db = createDatabase(createPostgresDatabaseAdapter(pool)) +``` + +Use `db.query(...)`, relation loading, and transactions from `remix/data-table`. +Import any driver-specific types you need directly from `pg`. + +## Adapter Capabilities + +`data-table-postgres` reports this capability set by default: + +- `returning: true` +- `savepoints: true` +- `upsert: true` +- `transactionalDdl: true` +- `migrationLock: true` + +## Advanced Usage + +### Transaction Options + +Transaction options are passed through to the adapter as hints. + +```ts +await db.transaction(async (txDb) => txDb.exec('select 1'), { + isolationLevel: 'serializable', + readOnly: false, +}) +``` + +## Related Packages + +- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - + Core query/relations API +- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - + Schema parsing and validation +- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - + MySQL adapter +- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - + SQLite adapter + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/data-table-sqlite.md b/docs/agents/remix/data-table-sqlite.md deleted file mode 100644 index 493cb21..0000000 --- a/docs/agents/remix/data-table-sqlite.md +++ /dev/null @@ -1,50 +0,0 @@ -# data-table-sqlite - -Source: https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite - -## README - -SQLite adapter for `remix/data-table`. - -## Installation - -```sh -npm i remix better-sqlite3 -``` - -## Usage - -```ts -import Database from 'better-sqlite3' -import { createDatabase } from 'remix/data-table' -import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite' - -let sqlite = new Database('app.db') -let db = createDatabase(createSqliteDatabaseAdapter(sqlite)) -``` - -## Cloudflare D1 note - -`remix/data-table-sqlite` expects a `better-sqlite3` connection object. For D1, -use a D1-specific adapter (for this repo, `worker/d1-data-table-adapter.ts`) -instead of `createSqliteDatabaseAdapter(...)`. - -## Default capabilities - -- `returning: true` -- `savepoints: true` -- `upsert: true` - -## Related packages - -- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) -- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) -- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/data-table-sqlite/changelog.md b/docs/agents/remix/data-table-sqlite/changelog.md new file mode 100644 index 0000000..890b9ae --- /dev/null +++ b/docs/agents/remix/data-table-sqlite/changelog.md @@ -0,0 +1,83 @@ +# `data-table-sqlite` CHANGELOG + +This is the changelog for +[`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite). +It follows [semantic versioning](https://semver.org/). + +## v0.4.0 + +### Minor Changes + +- Widened `createSqliteDatabaseAdapter` to accept synchronous SQLite clients + that match the shared `prepare`/`exec` surface used by Node's `node:sqlite`, + Bun's `bun:sqlite`, and compatible clients. The package no longer requires + `better-sqlite3` as an optional peer dependency. + +## v0.3.0 + +### Minor Changes + +- BREAKING CHANGE: Removed adapter options + + **Affected APIs** + - `SqliteDatabaseAdapterOptions` type: removed + - `createSqliteDatabaseAdapter` function: `options` arg removed + - `SqliteDatabaseAdapter` constructor: `options` arg removed + + **Why** + + Adapter options existed solely for tests to override adapter capabilities. If + you must override capabilities, you can do so directly via mutation: + + ```ts + let adapter = createSqliteDatabaseAdapter(sqlite) + adapter.capabilities = { + ...adapter.capabilities, + returning: false, + } + ``` + +## v0.2.0 + +### Minor Changes + +- Add first-class migration execution support to the sqlite adapter. It now + compiles and executes `DataMigrationOperation` plans for + `remix/data-table/migrations`, including create/alter/drop table and index + flows, migration journal writes, and adapter-managed DDL execution for + migrations. + + Normal reads/writes continue through `execute(...)`, while migration/DDL work + runs through `migrate(...)`. + + SQL compilation remains adapter-owned and can share helpers from + `remix/data-table/sql-helpers`. + +- Add transaction-aware migration introspection to the sqlite adapter. + + `hasTable(table, transaction?)` and `hasColumn(table, column, transaction?)` + now accept a transaction token, validate it, and execute against the migration + transaction when provided so schema checks line up with the active migration + transaction. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`data-table@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.2.0) + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/data-table-sqlite`. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0) + +## v0.1.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/data-table-sqlite/index.md b/docs/agents/remix/data-table-sqlite/index.md new file mode 100644 index 0000000..6c5a643 --- /dev/null +++ b/docs/agents/remix/data-table-sqlite/index.md @@ -0,0 +1,98 @@ + + +# data-table-sqlite + +SQLite adapter for +[`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table). +Use this package when you want `data-table` APIs backed by a synchronous SQLite +client. + +## Features + +- **Native Runtime SQLite Support**: Works with Node's `node:sqlite` + `DatabaseSync`, Bun's `bun:sqlite` `Database`, and compatible synchronous + SQLite clients +- **Full `data-table` API Support**: Queries, relations, writes, and + transactions +- **Adapter-Owned Compiler**: SQL compilation lives in this adapter, with + optional shared pure helpers from `data-table` +- **Migration DDL Support**: Compiles and executes `DataMigrationOperation` + operations for `remix/data-table/migrations` +- **SQLite Capabilities Enabled By Default**: + - `returning: true` + - `savepoints: true` + - `upsert: true` + - `transactionalDdl: true` + - `migrationLock: false` + +## Installation + +```sh +npm i remix +``` + +## Usage + +### Node + +```ts +import { DatabaseSync } from 'node:sqlite' +import { createDatabase } from 'remix/data-table' +import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite' + +let sqlite = new DatabaseSync('app.db') +let db = createDatabase(createSqliteDatabaseAdapter(sqlite)) +``` + +### Bun + +```ts +import { Database } from 'bun:sqlite' +import { createDatabase } from 'remix/data-table' +import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite' + +let sqlite = new Database('app.db') +let db = createDatabase(createSqliteDatabaseAdapter(sqlite)) +``` + +This is a good fit for local development, embedded deployments, and single-node +services. Import any driver-specific types you need directly from your runtime's +SQLite module. + +## Adapter Capabilities + +`data-table-sqlite` reports this capability set by default: + +- `returning: true` +- `savepoints: true` +- `upsert: true` +- `transactionalDdl: true` +- `migrationLock: false` + +## Advanced Usage + +### In-Memory Database For Tests + +```ts +import { DatabaseSync } from 'node:sqlite' +import { createDatabase } from 'remix/data-table' +import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite' + +let sqlite = new DatabaseSync(':memory:') +let db = createDatabase(createSqliteDatabaseAdapter(sqlite)) +``` + +## Related Packages + +- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - + Core query/relations API +- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - + Schema parsing and validation +- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - + PostgreSQL adapter +- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - + MySQL adapter + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/data-table.md b/docs/agents/remix/data-table.md deleted file mode 100644 index 02c9f09..0000000 --- a/docs/agents/remix/data-table.md +++ /dev/null @@ -1,86 +0,0 @@ -# data-table - -Source: https://github.com/remix-run/remix/tree/main/packages/data-table - -## README - -`data-table` is a typed relational query toolkit with a shared API across SQL -adapters. - -## Features - -- Query builder + CRUD helpers -- Typed selects and relation loading -- Schema-validated writes via `remix/data-schema` -- Transaction support and adapter capability detection - -## Installation - -```sh -npm i remix -``` - -Install a database driver for your adapter: - -- Postgres: `npm i pg` -- MySQL: `npm i mysql2` -- SQLite: `npm i better-sqlite3` - -## Usage - -```ts -import * as s from 'remix/data-schema' -import { createDatabase, createTable } from 'remix/data-table' -import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite' -import Database from 'better-sqlite3' - -let users = createTable({ - name: 'users', - columns: { - id: s.string(), - email: s.string(), - }, -}) - -let sqlite = new Database('app.db') -let db = createDatabase(createSqliteDatabaseAdapter(sqlite)) - -let allUsers = await db.query(users).select().all() -``` - -## epicflare note (Cloudflare D1) - -This repository uses `remix/data-table` for D1-backed app and mock-server data -access, but D1 is not a `better-sqlite3` connection. We use a custom adapter at -`worker/d1-data-table-adapter.ts` and build the runtime in `worker/db.ts`: - -```ts -import { createDatabase } from 'remix/data-table' -import { createD1DataTableAdapter } from '#worker/d1-data-table-adapter.ts' - -let db = createDatabase(createD1DataTableAdapter(env.APP_DB)) -``` - -Table metadata and shared table definitions live in `worker/db.ts`. - -## Adapter packages - -- `remix/data-table-postgres` -- `remix/data-table-mysql` -- `remix/data-table-sqlite` - -## Related packages - -- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - - parsing and validation primitives used by table definitions -- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) -- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) -- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/data-table/changelog.md b/docs/agents/remix/data-table/changelog.md new file mode 100644 index 0000000..f5a013f --- /dev/null +++ b/docs/agents/remix/data-table/changelog.md @@ -0,0 +1,117 @@ +# `data-table` CHANGELOG + +This is the changelog for +[`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table). +It follows [semantic versioning](https://semver.org/). + +## v0.2.0 + +### Minor Changes + +- BREAKING CHANGE: Rename adapter operation contracts and fields. + + `AdapterStatement` becomes `DataManipulationOperation`, and `statement` + becomes `operation`. + + Add separate adapter execution methods for DML and migration/DDL operations: + `execute` for `DataManipulationOperation` requests and `migrate` for + `DataMigrationOperation` requests. + + Add adapter introspection methods with optional transaction context: + `hasTable(table, transaction?)` and `hasColumn(table, column, transaction?)`. + +- BREAKING CHANGE: Replace the public `QueryBuilder` API with `Query` objects + that can be created with `query(table)` and executed with `db.exec(...)`. + + `db.query(table)` still provides the same chainable ergonomics, but it now + returns the public `Query` class in a database-bound form instead of a + separate `QueryBuilder` type. `db.exec(...)` now accepts only raw SQL or + `Query` values, and unbound terminal methods like `first()`, `count()`, + `exists()`, `insert()`, `update()`, and `delete()` return `Query` objects + instead of separate command descriptor types. + + The incidental `QueryMethod` type export has also been removed; use + `Database['query']` or `QueryForTable` when you need that type shape. + +- BREAKING CHANGE: Remove the `@remix-run/data-table/sql` export. Import + `SqlStatement`, `sql`, and `rawSql` from `@remix-run/data-table` instead. + + `@remix-run/data-table/sql-helpers` remains available as the adapter-facing + SQL helper module. + +- BREAKING CHANGE: Rename the top-level table-definition helper from + `createTable(...)` to `table(...)` and switch column definitions to + `column(...)` builders. Runtime validation is now optional and table-scoped + via `validate({ operation, tableName, value })`. + + Remove `~standard` table-schema compatibility and + `getTableValidationSchemas(...)`, and stop runtime validation/coercion for + predicate values. + +- `@remix-run/data-table` now exports `Database` as the runtime class instead of + separating the runtime implementation from a structural `Database` type. You + can construct databases directly with `new Database(adapter, options)` or keep + using `createDatabase(adapter, options)`, which now delegates to the class + constructor. + +- Add a first-class migration system under `remix/data-table/migrations` with: + - `createMigration(...)` and timestamp-based migration loading + - chainable `column` builders plus schema APIs for create, alter, drop, and + index work + - `createMigrationRunner(adapter, migrations)` for `up`, `down`, `status`, and + `dryRun` + - migration journaling, checksum tracking, and optional Node loading from + `remix/data-table/migrations/node` + + Migration callbacks now use split handles: `{ db, schema }`. + - `db` is the immediate data runtime + (`query/create/update/delete/exec/transaction`) + - `schema` owns migration operations like `createTable`, `alterTable`, `plan`, + and introspection + + Migration-time DDL, DML, and introspection now share the same transaction + token when migration transactions are enabled. In `dryRun`, schema + introspection (`schema.hasTable` / `schema.hasColumn`) reads live + adapter/database state and does not simulate pending dry-run operations. + + Add public subpath exports for migrations, Node migration loading, SQL + helpers, operators, and SQL builders. SQL compilation stays adapter-owned, + while shared SQL compiler helpers remain available from + `remix/data-table/sql-helpers`. + + `@remix-run/data-table/migrations` no longer exports a separate `Database` + type alias. Migration callbacks still receive `context.db` as the main + `Database` runtime, so if you need the type directly, import `Database` from + `@remix-run/data-table` instead. + +- Add optional table lifecycle callbacks for write/delete/read flows: + `beforeWrite`, `afterWrite`, `beforeDelete`, `afterDelete`, and `afterRead`. + + Add `fail(...)` as a helper for returning structured validation/lifecycle + issues from `validate(...)`, `beforeWrite(...)`, and `beforeDelete(...)`. + +## v0.1.0 + +### Minor Changes + +- Add support for cross-schema column resolution + +- Initial release of `@remix-run/data-table`. + +- Make `createTable()` results Standard Schema-compatible so tables can be used + directly with `parse()`/`parseSafe()` from `remix/data-schema`. + + Table parsing now mirrors write validation semantics used by + `create()`/`update()`: partial objects are accepted, provided values are + parsed via column schemas, and unknown columns are rejected. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`data-schema@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-schema@0.1.0) + +## v0.1.0 + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/data-table/index.md b/docs/agents/remix/data-table/index.md new file mode 100644 index 0000000..6d177ac --- /dev/null +++ b/docs/agents/remix/data-table/index.md @@ -0,0 +1,651 @@ + + +# data-table + +Typed relational query toolkit for JavaScript runtimes. + +## Features + +- **One API Across Databases**: Same query and relation APIs across PostgreSQL, + MySQL, and SQLite adapters +- **One Query API**: Build reusable `Query` objects with `query(table)` and + execute them with `db.exec(...)`, or use `db.query(table)` as shorthand +- **Type-Safe Reads**: Typed `select`, relation loading, and predicate keys +- **Optional Runtime Validation**: Add `validate(context)` at the table level + for create/update validation and coercion +- **Relation-First Queries**: `hasMany`, `hasOne`, `belongsTo`, + `hasManyThrough`, and nested eager loading +- **Safe Scoped Writes**: `update`/`delete` with `orderBy`/`limit` run safely in + a transaction +- **First-Class Migrations**: Up/down migrations with schema builders, runner + controls, and dry-run planning +- **Raw SQL Escape Hatch**: Execute SQL directly with `db.exec(sql\`...\`)` + +`data-table` gives you two complementary APIs: + +- [**Query Objects**](#query-objects) for expressive joins, aggregates, eager + loading, and scoped writes +- [**CRUD Helpers**](#crud-helpers) for common create/read/update/delete flows + (`find`, `create`, `update`, `delete`) + +Both APIs are type-safe. Runtime validation is opt-in with table-level +`validate(context)`. + +## Installation + +```sh +npm i remix +npm i pg +# or +npm i mysql2 +# or +# use the SQLite client built into your runtime +``` + +## Setup + +Define tables once, then create a database with an adapter. + +```ts +import { Pool } from 'pg' +import { + column as c, + createDatabase, + hasMany, + query, + table, +} from 'remix/data-table' +import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres' + +let users = table({ + name: 'users', + columns: { + id: c.uuid(), + email: c.varchar(255), + role: c.enum(['customer', 'admin']), + created_at: c.integer(), + }, +}) + +let orders = table({ + name: 'orders', + columns: { + id: c.uuid(), + user_id: c.uuid(), + status: c.enum(['pending', 'processing', 'shipped', 'delivered']), + total: c.decimal(10, 2), + created_at: c.integer(), + }, +}) + +let userOrders = hasMany(users, orders) + +let pool = new Pool({ connectionString: process.env.DATABASE_URL }) +let db = createDatabase(createPostgresDatabaseAdapter(pool)) +``` + +## Query Objects + +Use `query(table)` when you want to build a standalone reusable query object. +Execute it later with `db.exec(query)`. Use `db.query(table)` when you want the +same chainable `Query` already bound to a database instance. + +### Standalone Query Builder + +`query(table)` is the primary query-builder API. It gives you an unbound `Query` +value that can be composed, stored, reused, and executed against any compatible +database instance. + +```ts +import { eq, ilike, query } from 'remix/data-table' + +let pendingOrdersForExampleUsers = query(orders) + .join(users, eq(orders.user_id, users.id)) + .where({ status: 'pending' }) + .where(ilike(users.email, '%@example.com')) + .select({ + orderId: orders.id, + customerEmail: users.email, + total: orders.total, + placedAt: orders.created_at, + }) + .orderBy(orders.created_at, 'desc') + .limit(20) + +let recentPendingOrders = await db.exec(pendingOrdersForExampleUsers) +``` + +Unbound queries stay lazy until you pass them to `db.exec(...)`: + +```ts +let shippedCustomerQuery = query(users) + .where({ role: 'customer' }) + .with({ + recentOrders: userOrders + .where({ status: 'shipped' }) + .orderBy('created_at', 'desc') + .limit(3), + }) + +let customers = await db.exec(shippedCustomerQuery) + +// customers[0].recentOrders is fully typed +``` + +The same standalone query builder also handles terminal read and write +operations: + +```ts +let nextPendingOrder = await db.exec( + query(orders) + .where({ status: 'pending' }) + .orderBy('created_at', 'asc') + .first(), +) + +await db.exec( + query(orders) + .where({ status: 'pending' }) + .orderBy('created_at', 'asc') + .limit(100) + .update({ status: 'processing' }), +) +``` + +### Bound Query Shorthand + +If you already have a `db` instance in hand and do not need a standalone query +value, `db.query(table)` returns the same query builder already bound to that +database: + +```ts +let recentPendingOrders = await db + .query(orders) + .where({ status: 'pending' }) + .orderBy('created_at', 'desc') + .limit(20) + .all() +``` + +## CRUD Helpers + +`data-table` provides helpers for common create/read/update/delete operations. +Use these helpers for common operations without building a full query chain. + +### Read operations + +```ts +import { or } from 'remix/data-table' + +let user = await db.find(users, 'u_001') + +let firstPending = await db.findOne(orders, { + where: { status: 'pending' }, + orderBy: ['created_at', 'asc'], +}) + +let page = await db.findMany(orders, { + where: or({ status: 'pending' }, { status: 'processing' }), + orderBy: [ + ['status', 'asc'], + ['created_at', 'desc'], + ], + limit: 50, + offset: 0, +}) +``` + +`where` accepts the same single-table object/predicate inputs as +`query().where(...)`, and `orderBy` uses tuple form: + +- `['column', 'asc' | 'desc']` +- `[['columnA', 'asc'], ['columnB', 'desc']]` + +### Create helpers + +```ts +// Default: metadata (affectedRows/insertId) +let createResult = await db.create(users, { + id: 'u_002', + email: 'sam@example.com', + role: 'customer', + created_at: Date.now(), +}) + +// Return a typed row (with optional relations) +let createdUser = await db.create( + users, + { + id: 'u_003', + email: 'pat@example.com', + role: 'customer', + created_at: Date.now(), + }, + { + returnRow: true, + with: { recentOrders: userOrders.orderBy('created_at', 'desc').limit(1) }, + }, +) + +// Bulk insert metadata +let createManyResult = await db.createMany(orders, [ + { + id: 'o_101', + user_id: 'u_002', + status: 'pending', + total: 24.99, + created_at: Date.now(), + }, + { + id: 'o_102', + user_id: 'u_003', + status: 'pending', + total: 48.5, + created_at: Date.now(), + }, +]) + +// Return inserted rows (requires adapter RETURNING support) +let insertedRows = await db.createMany( + orders, + [ + { + id: 'o_103', + user_id: 'u_003', + status: 'pending', + total: 12, + created_at: Date.now(), + }, + ], + { returnRows: true }, +) +``` + +`createMany`/`insertMany` throw when every row in the batch is empty (no +explicit values). + +### Update and delete helpers + +```ts +let updatedUser = await db.update(users, 'u_003', { role: 'admin' }) + +let updateManyResult = await db.updateMany( + orders, + { status: 'processing' }, + { + where: { status: 'pending' }, + orderBy: ['created_at', 'asc'], + limit: 25, + }, +) + +let deletedUser = await db.delete(users, 'u_002') + +let deleteManyResult = await db.deleteMany(orders, { + where: { status: 'delivered' }, + orderBy: [['created_at', 'asc']], + limit: 200, +}) +``` + +`db.update(...)` throws when the target row cannot be found. + +Return behavior: + +- `find`/`findOne` -> row or `null` +- `findMany` -> rows +- `create` -> `WriteResult` by default, row when `returnRow: true` +- `createMany` -> `WriteResult` by default, rows when `returnRows: true` (not + supported in MySQL because it doesn't support `RETURNING`) +- `update` -> updated row (throws when target row is missing) +- `updateMany`/`deleteMany` -> `WriteResult` +- `delete` -> `boolean` + +### Validation and Lifecycle + +Validation is optional and table-scoped. Define `validate(context)` to +validate/coerce write payloads, and add lifecycle callbacks when you need custom +read/write/delete behavior. + +```ts +import { column as c, fail, table } from 'remix/data-table' + +let payments = table({ + name: 'payments', + columns: { + id: c.uuid(), + amount: c.decimal(10, 2), + }, + beforeWrite({ value }) { + return { + value: { + ...value, + amount: + typeof value.amount === 'string' ? value.amount.trim() : value.amount, + }, + } + }, + validate({ operation, value }) { + if (operation === 'create' && typeof value.amount === 'string') { + let amount = Number(value.amount) + + if (!Number.isFinite(amount)) { + return fail('Expected a numeric amount', ['amount']) + } + + return { value: { ...value, amount } } + } + + return { value } + }, + beforeDelete({ where }) { + if (where.length === 0) { + return fail('Refusing unscoped delete') + } + }, + afterRead({ value }) { + if (!('amount' in value)) { + return { value } + } + + return { + value: { + ...value, + // Example read-time shaping + amount: + typeof value.amount === 'number' + ? Math.round(value.amount * 100) / 100 + : value.amount, + }, + } + }, +}) +``` + +Use `fail(...)` in hooks when you want to return issues without manually +building `{ issues: [...] }`. + +Validation and lifecycle semantics: + +- Write order is + `beforeWrite -> validate -> timestamp/default touch -> execute -> afterWrite` +- `validate` runs for writes (`create`, `createMany`, `insert`, `insertMany`, + `update`, `updateMany`, `upsert`) +- Hook context includes `{ operation: 'create' | 'update', tableName, value }` +- Write payloads are partial objects +- Unknown columns fail validation before and after hook processing +- `beforeDelete` can veto deletes by returning `{ issues }` +- `afterDelete` runs after successful deletes with `affectedRows` +- `afterRead` runs for each loaded row (root rows, eager-loaded relation rows, + and write-returning rows) +- `afterRead` receives the current read shape, which may be partial/projection + rows; guard field access accordingly +- Predicate values (`where`, `having`, join predicates) are not + runtime-validated +- Lifecycle callbacks are synchronous; returning a Promise throws a validation + error +- Callback validation errors include `metadata.source` (`beforeWrite`, + `validate`, `beforeDelete`, `afterRead`, etc.) for easier debugging +- Callbacks do not introduce implicit transactions (use `db.transaction(...)` + when you need rollback guarantees) + +## Transactions + +```ts +await db.transaction(async (tx) => { + let user = await tx.create( + users, + { + id: 'u_010', + email: 'new@example.com', + role: 'customer', + created_at: Date.now(), + }, + { returnRow: true }, + ) + + await tx.create(orders, { + id: 'o_500', + user_id: user.id, + status: 'pending', + total: 79, + created_at: Date.now(), + }) +}) +``` + +## Migrations + +`data-table` includes a first-class migration system under +`remix/data-table/migrations`. Migrations are adapter-driven: adapters execute +SQL for their dialect/runtime, and SQL compilation is handled by adapter-owned +compilers (with optional shared pure helpers from `data-table`). For adapter +authors (including third-party adapters), shared SQL helper utilities are +available at `remix/data-table/sql-helpers`. + +### Example Setup + +```txt +app/ + db/ + migrations/ + 20260228090000_create_users.ts + 20260301113000_add_user_status.ts + migrate.ts +``` + +- Keep migration files in one directory (for example `app/db/migrations`). +- Name each file as `YYYYMMDDHHmmss_name.ts` (or `.js`, `.mjs`, `.cjs`, `.cts`). +- Each file must `default` export `createMigration(...)`; `id` and `name` are + inferred from filename. + +### Migration File Example + +```ts +import { column as c, table } from 'remix/data-table' +import { createMigration } from 'remix/data-table/migrations' + +let users = table({ + name: 'users', + columns: { + id: c.integer().primaryKey(), + email: c.varchar(255).notNull().unique(), + created_at: c.timestamp({ withTimezone: true }).defaultNow(), + }, +}) + +export default createMigration({ + async up({ db, schema }) { + await schema.createTable(users) + await schema.createIndex(users, 'email', { unique: true }) + + if (db.adapter.dialect === 'sqlite') { + await db.exec('pragma foreign_keys = on') + } + }, + async down({ schema }) { + await schema.dropTable(users, { ifExists: true }) + }, +}) +``` + +### Runner Script Example + +In `app/db/migrate.ts`: + +```ts +import path from 'node:path' +import { Pool } from 'pg' +import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres' +import { createMigrationRunner } from 'remix/data-table/migrations' +import { loadMigrations } from 'remix/data-table/migrations/node' + +let directionArg = process.argv[2] ?? 'up' +let direction = directionArg === 'down' ? 'down' : 'up' +let to = process.argv[3] + +let pool = new Pool({ connectionString: process.env.DATABASE_URL }) +let adapter = createPostgresDatabaseAdapter(pool) +let migrations = await loadMigrations(path.resolve('app/db/migrations')) +let runner = createMigrationRunner(adapter, migrations) + +try { + let result = + direction === 'up' ? await runner.up({ to }) : await runner.down({ to }) + console.log(direction + ' complete', { + applied: result.applied.map((entry) => entry.id), + reverted: result.reverted.map((entry) => entry.id), + }) +} finally { + await pool.end() +} +``` + +Use `journalTable` if you want a custom migrations journal table name: + +```ts +let runner = createMigrationRunner(adapter, migrations, { + journalTable: 'app_migrations', +}) +``` + +Run it with your runtime, for example: + +```sh +node ./app/db/migrate.ts up +node ./app/db/migrate.ts up 20260301113000 +node ./app/db/migrate.ts down +node ./app/db/migrate.ts down 20260228090000 +``` + +Use `step` when you want bounded rollforward/rollback behavior instead of a +target id: + +```ts +await runner.up({ step: 1 }) +await runner.down({ step: 1 }) +``` + +`to` and `step` are mutually exclusive. Use one or the other for a given run. + +Use `dryRun` to compile and inspect the SQL plan without applying migrations: + +```ts +let dryRunResult = await runner.up({ dryRun: true }) +console.log(dryRunResult.sql) +``` + +When migration transactions are enabled, migration-time +`schema.createTable(...)`, `db.exec(...)`, query-builder data operations, and +`schema.hasTable(...)` / `schema.hasColumn(...)` all run in the same adapter +transaction context. + +You can also pass a pre-built SQL statement into `schema.plan(...)` when +authoring migrations: + +```ts +import { sql } from 'remix/data-table' + +await schema.plan( + sql`update users set status = ${'active'} where status is null`, +) +``` + +You can run lightweight schema checks inside a migration with +`schema.hasTable(...)` and `schema.hasColumn(...)` when you need defensive +conditional behavior. Methods that take a table name accept either a string +(`'app.users'`) or a `table(...)` object. + +In `dryRun` mode, introspection methods still check the live database state. +They do not simulate tables/columns from pending operations in the current +dry-run plan. + +For key-oriented migration APIs, single-column and compound forms are both +supported: + +```ts +await schema.alterTable(users, (table) => { + table.addPrimaryKey('id') + table.addForeignKey('account_id', 'accounts', 'id') + table.addForeignKey(['tenant_id', 'account_id'], 'accounts', [ + 'tenant_id', + 'id', + ]) +}) +``` + +Constraint and index names are optional in migration APIs. When omitted, +`data-table` generates deterministic names for primary keys, uniques, foreign +keys, checks, and indexes. + +This is useful when you want to: + +- Review generated SQL in CI before deploying +- Verify migration ordering and target/step selection +- Audit dialect-specific SQL differences across adapters + +For non-filesystem runtimes, register migrations manually: + +```ts +import { + createMigrationRegistry, + createMigrationRunner, +} from 'remix/data-table/migrations' +import createUsers from './db/migrations/20260228090000_create_users.ts' + +let registry = createMigrationRegistry() +registry.register({ + id: '20260228090000', + name: 'create_users', + migration: createUsers, +}) + +// adapter from createPostgresDatabaseAdapter/createMysqlDatabaseAdapter/createSqliteDatabaseAdapter +let runner = createMigrationRunner(adapter, registry) +await runner.up() +``` + +## Raw SQL Escape Hatch + +```ts +import { rawSql, sql } from 'remix/data-table' + +await db.exec(sql`select * from users where id = ${'u_001'}`) +await db.exec( + rawSql('update users set role = ? where id = ?', ['admin', 'u_001']), +) +``` + +Use `sql` when you need raw SQL plus safe value interpolation: + +```ts +import { sql } from 'remix/data-table' + +let email = input.email +let minCreatedAt = input.minCreatedAt + +let result = await db.exec(sql` + select id, email + from users + where email = ${email} + and created_at >= ${minCreatedAt} +`) +``` + +`sql` keeps values parameterized per adapter dialect, so you can avoid manual +string concatenation. + +## Related Packages + +- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - + Optional schema parsing you can use inside table-level `validate(...)` hooks +- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - + PostgreSQL adapter +- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - + MySQL adapter +- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - + SQLite adapter + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/fetch-proxy.md b/docs/agents/remix/fetch-proxy.md deleted file mode 100644 index 30f4d9b..0000000 --- a/docs/agents/remix/fetch-proxy.md +++ /dev/null @@ -1,76 +0,0 @@ -# fetch-proxy - -Source: https://github.com/remix-run/remix/tree/main/packages/fetch-proxy - -## README - -`fetch-proxy` is an HTTP proxy for the -[JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). - -HTTP proxies are essential for many web architectures: load balancing, API -gateways, development servers that forward to backend services, and middleware -that needs to intercept and modify traffic. Traditional proxy implementations -often require platform-specific APIs or complex server setups. - -In the context of servers, an HTTP proxy server is a server that forwards all -requests it receives to another server and returns the responses it receives. -When you think about it this way, a -[`fetch` function](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) -is like a mini proxy server sitting right there in your code. You send it -requests, it goes and talks to some other server, and it gives you back the -response it received. - -`fetch-proxy` allows you to easily create `fetch` functions that act as proxies -to "target" servers using the familiar web-standard Fetch API. - -## Features - -- **Web Standards** - Built on the standard - [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) -- **Cookie Rewriting** - Supports rewriting `Set-Cookie` headers received from - target server -- **Forwarding Headers** - Supports `X-Forwarded-Proto` and `X-Forwarded-Host` - headers -- **Custom Fetch** - Supports custom `fetch` implementations - -## Installation - -Install from [npm](https://www.npmjs.com/): - -```sh -npm i @remix-run/fetch-proxy -``` - -## Usage - -```ts -import { createFetchProxy } from '@remix-run/fetch-proxy' - -// Create a proxy that sends all requests through to remix.run -let proxy = createFetchProxy('https://remix.run') - -// This fetch handler is probably running as part of your server somewhere... -function handleFetch(request: Request): Promise { - return proxy(request) -} - -// Test it out by manually throwing a Request at it -let response = await handleFetch(new Request('https://shopify.com')) - -let text = await response.text() -let title = text.match(/([^<]+)<\/title>/)[1] -assert(title.includes('Remix')) -``` - -## Related Packages - -- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) - - Build HTTP servers for Node.js using the web fetch API - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) - -## Navigation - -- [Remix package index](./index.md) diff --git a/docs/agents/remix/fetch-proxy/changelog.md b/docs/agents/remix/fetch-proxy/changelog.md new file mode 100644 index 0000000..355b222 --- /dev/null +++ b/docs/agents/remix/fetch-proxy/changelog.md @@ -0,0 +1,65 @@ +# `fetch-proxy` CHANGELOG + +This is the changelog for +[`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy). +It follows [semantic versioning](https://semver.org/). + +## v0.8.0 + +### Minor Changes + +- Add an `X-Forwarded-Port` header when `xForwardedHeaders` is enabled. + +## v0.7.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.7.0 (2025-11-05) + +- Move `@remix-run/headers` to `peerDependencies` +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.6.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v0.5.0 (2025-07-24) + +- Renamed package from `@mjackson/fetch-proxy` to `@remix-run/fetch-proxy` +- FIX: A regression that stopped forwarding the method from an exising request + object +- Forward additional properties from existing request objects passed to the + proxy, including: + - cache + - credentials + - integrity + - keepalive + - mode + - redirect + - referrer + - referrerPolicy + - signal + +## v0.4.0 (2025-07-11) + +- Forward all additional options to the proxied request object + +## v0.3.0 (2025-06-10) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v0.2.0 (2024-11-14) + +- Added CommonJS build + +## v0.1.0 (2024-09-12) + +- Initial release diff --git a/docs/agents/remix/fetch-proxy/index.md b/docs/agents/remix/fetch-proxy/index.md new file mode 100644 index 0000000..276f1b5 --- /dev/null +++ b/docs/agents/remix/fetch-proxy/index.md @@ -0,0 +1,54 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/fetch-proxy --> + +# fetch-proxy + +HTTP proxy utilities built on the web +[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). Use +`fetch-proxy` to create `fetch` handlers that forward requests to target servers +while optionally rewriting headers and cookies. + +## Features + +- **Web Standards** - Built on the standard + [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) +- **Cookie Rewriting** - Supports rewriting `Set-Cookie` headers received from + target server +- **Forwarding Headers** - Supports `X-Forwarded-Proto`, `X-Forwarded-Host`, and + `X-Forwarded-Port` headers +- **Custom Fetch** - Supports custom `fetch` implementations + +## Installation + +```sh +npm i remix +``` + +## Usage + +```ts +import { createFetchProxy } from 'remix/fetch-proxy' + +// Create a proxy that sends all requests through to remix.run +let proxy = createFetchProxy('https://remix.run') + +// This fetch handler is probably running as part of your server somewhere... +function handleFetch(request: Request): Promise<Response> { + return proxy(request) +} + +// Test it out by manually throwing a Request at it +let response = await handleFetch(new Request('https://shopify.com')) + +let text = await response.text() +let title = text.match(/<title>([^<]+)<\/title>/)[1] +assert(title.includes('Remix')) +``` + +## Related Packages + +- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) - + Build HTTP servers for 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/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 `<input name="_method" value="...">` 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('<h1>Hello</h1>') -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 = '<script>alert(1)</script>' -let response = createHtmlResponse(html`<h1>${unsafe}</h1>`, { 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 = '<b>Bold</b>' -let content = html.raw`<div class="content">${safeHtml}</div>` -let response = createHtmlResponse(content) - -// This is particularly useful when building HTML from multiple safe fragments -let header = '<header>Title</header>' -let body = '<main>Content</main>' -let footer = '<footer>Footer</footer>' -let page = html.raw` - <!DOCTYPE html> - <html> - <body> - ${header} - ${body} - ${footer} - </body> - </html> -` - -// You can nest html.raw inside html to preserve SafeHtml fragments -let icon = html.raw`<svg>...</svg>` -let button = html`<button>${icon} Click me</button>` // 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/changelog.md b/docs/agents/remix/fetch-router/changelog.md new file mode 100644 index 0000000..6c6b148 --- /dev/null +++ b/docs/agents/remix/fetch-router/changelog.md @@ -0,0 +1,848 @@ +# `fetch-router` CHANGELOG + +This is the changelog for +[`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router). +It follows [semantic versioning](https://semver.org/). + +## v0.18.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`route-pattern@0.20.1`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.20.1) + +## v0.18.0 + +### Minor Changes + +- BREAKING CHANGE: Action objects now use `handler` instead of `action`. + + This applies to the object form accepted by `router.get(...)`, + `router.post(...)`, and `router.map(...)`, and to `BuildAction` object + definitions. + +- BREAKING CHANGE: Remove `context.storage`, `context.session`, + `context.sessionStarted`, `context.formData`, and `context.files` from + `@remix-run/fetch-router`, and rename `createStorageKey(...)` to + `createContextKey(...)`. + + `RequestContext` now provides request-scoped context methods directly + (`context.get(key)`, `context.set(key, value)`, and `context.has(key)`), using + keys created with `createContextKey(...)` or constructors like `Session` and + `FormData`. + + Session middleware now stores the request session with + `context.set(Session, session)`, and form-data middleware now stores parsed + form data with `context.set(FormData, formData)`. Uploaded files are read from + `context.get(FormData)` using `get(...)`/`getAll(...)`. + + `RequestContext` is now generic over route params and typed context entries + (`RequestContext<{ id: string }, entries>`), and no longer accepts a + request-method generic (`RequestContext<'GET', ...>`). + +- BREAKING CHANGE: `router.map()` controllers for route maps now require a + single shape: an object with an `actions` property and optional `middleware`. + + Migration: Wrap existing controller objects in `actions`. Nested route maps + must also use nested controllers with `{ actions, middleware? }`. + +- `fetch-router` now threads request context types through `Router`, + `Controller`, and `BuildAction`, and exports helpers like `MiddlewareContext`, + `WithParams`, `MergeContext`, and `AnyParams` so apps can derive context + contracts from installed middleware. + +### Patch Changes + +- The `Action`/`BuildAction` object form accepted by `router.get(...)`, + `router.post(...)`, and `router.map(...)` now uses `{ handler, middleware? }`, + so you can omit `middleware` entirely instead of writing `middleware: []` when + you do not need route middleware. + +- Bumped `@remix-run/*` dependencies: + - [`route-pattern@0.20.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.20.0) + +## v0.17.0 + +### Minor Changes + +- Expose `context.router` on request context + + Each router request context now gets the owning `Router` assigned as + `context.router` by `createRouter()` when `fetch()` is called. This lets + framework helpers read router state directly from `RequestContext` instead of + requiring app-level middleware to store the router in `context.storage`. + +- Added a new `@remix-run/fetch-router/routes` export exporting route creation + utilities + + This has been decoupled from the main `@remix-run/fetch-router` exports so + that it can be used by application `routes.ts` files intended to be loaded by + the client, without pulling in server-side-specific underlying packages such + as `@remix-run/session`. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`route-pattern@0.19.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.19.0) + +## v0.16.0 + +### Minor Changes + +- BREAKING CHANGE: Remove `Router.size` property + + `Matcher`s no longer keep track of size, so `Router` cannot wrap + `Matcher.size` anymore. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/route-pattern@0.18.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.18.0) + +## v0.15.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.15.0 + +### Minor Changes + +- BREAKING CHANGE: `RequestContext.headers` now returns a standard `Headers` + instance instead of the `SuperHeaders`/`Headers` subclass from + `@remix-run/headers`. As a result, the `@remix-run/headers` peer dependency + has now been removed. + + If you were relying on the type-safe property accessors on + `RequestContext.headers`, you should use the new parse functions from + `@remix-run/headers` instead: + + ```ts + // Before: + router.get('/api/users', (context) => { + let acceptsJson = context.headers.accept.accepts('application/json') + // ... + }) + + // After: + import { Accept } from '@remix-run/headers' + + router.get('/api/users', (context) => { + let accept = Accept.from(context.headers.get('accept')) + let acceptsJson = accept.accepts('application/json') + // ... + }) + ``` + +## v0.14.0 (2025-12-18) + +- BREAKING CHANGE: Remove `BuildRequestHandler` type. Use `RequestHandler` type + directly instead. + +- BREAKING CHANGE: Remove `T` generic parameter from `RequestHandler` type. + Request handlers always return a `Response`. + +- Export the `MatchData` type from the public API. This type is required when + creating custom matchers for use with the router's `matcher` option. + +## v0.13.0 (2025-12-01) + +- BREAKING CHANGE: Renamed "route handlers" terminology to "controller/action" + throughout the package. This is a breaking change for anyone using the types + or properties from this package. Update your code: + + ```tsx + // Before + import type { RouteHandlers } from '@remix-run/fetch-router' + + let routeHandlers = { + middleware: [auth()], + handlers: { + home() { + return new Response('Home') + }, + admin: { + middleware: [requireAdmin()], + handler() { + return new Response('Admin') + }, + }, + }, + } satisfies RouteHandlers<typeof routes> + + router.map(routes, routeHandlers) + + // After + import type { Controller } from '@remix-run/fetch-router' + + let controller = { + middleware: [auth()], + actions: { + home() { + return new Response('Home') + }, + admin: { + middleware: [requireAdmin()], + action() { + return new Response('Admin') + }, + }, + }, + } satisfies Controller<typeof routes> + + router.map(routes, controller) + ``` + + Summary of changes: + - `RouteHandlers` type => `Controller` + - `RouteHandler` type => `Action` + - `BuildRouteHandler` type => `BuildAction` + - `handlers` property => `actions` + - `handler` property => `action` + +- BREAKING CHANGE: Renamed `formAction` route helper to `form` and moved route + helpers to `lib/route-helpers/` subdirectory. Update your imports: + + ```tsx + // Before + import { route, formAction } from '@remix-run/fetch-router' + + let routes = route({ + login: formAction('/login'), + }) + + // After + import { route, form } from '@remix-run/fetch-router' + + let routes = route({ + login: form('/login'), + }) + ``` + + The `FormActionOptions` type has also been renamed to `FormOptions`. + +- BREAKING CHANGE: The `middleware` property is now required (not optional) in + controller and action objects that use the `{ middleware, actions }` or + `{ middleware, action }` format. This eliminates ambiguity when route names + like `action` collide with the `action` property name. + + ```tsx + // Before: { action } without middleware was allowed + router.any(routes.home, { + action() { + return new Response('Home') + }, + }) + + // After: just use a plain request handler function instead + router.any(routes.home, () => { + return new Response('Home') + }) + + // Before: { actions } without middleware was allowed + router.map(routes, { + actions: { + home() { + return new Response('Home') + }, + }, + }) + + // After: just use a plain controller object instead + router.map(routes, { + home() { + return new Response('Home') + }, + }) + + // With middleware, the syntax remains the same (but middleware is now required) + router.map(routes, { + middleware: [auth()], + actions: { + home() { + return new Response('Home') + }, + }, + }) + ``` + +- Add functional aliases for creating routes that respond to a single request + method + + ```tsx + import { del, get, patch, post } from '@remix-run/fetch-router' + + let routes = route({ + home: get('/'), + login: post('/login'), + logout: post('/logout'), + profile: { + show: get('/profile'), + edit: get('/profile/edit'), + update: patch('/profile'), + destroy: del('/profile'), + }, + }) + ``` + +## v0.12.0 (2025-11-25) + +- BREAKING CHANGE: Moved all response helpers to `@remix-run/response`. Update + your imports: + + ```tsx + // Before + import * as res from '@remix-run/fetch-router/response-helpers' + + res.file(file, request) + res.html(body) + res.redirect(location, status, headers) + + // After + import { createFileResponse } from '@remix-run/response/file' + import { createHtmlResponse } from '@remix-run/response/html' + import { createRedirectResponse } from '@remix-run/response/redirect' + + createFileResponse(file, request) + createHtmlResponse(body) + createRedirectResponse(location, status) + ``` + +- BREAKING CHANGE: Rename `InferRequestHandler` => `BuildRequestHandler` +- Add `exclude` option to `resource()` and `resources()` route map helpers + (#10858) + +## v0.11.0 (2025-11-21) + +- BREAKING CHANGE: `Router` is no longer exported as a class, use + `createRouter()` instead. + + ```tsx + // Before + import { Router } from '@remix-run/fetch-router' + let router = new Router() + + // After + import { createRouter } from '@remix-run/fetch-router' + let router = createRouter() + + // For type annotations, use the Router interface + import type { Router } from '@remix-run/fetch-router' + function setupRoutes(router: Router) { + // ... + } + ``` + + This change improves the ergonomics of the router by eliminating the need to + bind methods when passing `router.fetch` as a callback, for example in + `node-fetch-server`'s `createRequestListener(router.fetch)`. + +- Make `middleware` optional in route handler(s) objects passed to + `router.map()` + + ```tsx + // Before + router.map('/', { + middleware: [], // required + handler() { + return new Response('Home') + }, + }) + + // After + router.map('/', { + // middleware is optional! + handler() { + return new Response('Home') + }, + }) + ``` + +## v0.10.0 (2025-11-19) + +- BREAKING CHANGE: All middleware has been extracted into separate npm packages + for independent versioning and deployment. Update your imports: + + ```tsx + // Before + import { asyncContext } from '@remix-run/fetch-router/async-context-middleware' + import { formData } from '@remix-run/fetch-router/form-data-middleware' + import { logger } from '@remix-run/fetch-router/logger-middleware' + import { methodOverride } from '@remix-run/fetch-router/method-override-middleware' + import { session } from '@remix-run/fetch-router/session-middleware' + import { staticFiles } from '@remix-run/fetch-router/static-middleware' + + // After + import { asyncContext } from '@remix-run/async-context-middleware' + import { formData } from '@remix-run/form-data-middleware' + import { logger } from '@remix-run/logger-middleware' + import { methodOverride } from '@remix-run/method-override-middleware' + import { session } from '@remix-run/session-middleware' + import { staticFiles } from '@remix-run/static-middleware' + ``` + + Each middleware now has its own package with independent dependencies, + changelog, and versioning. + +- `html()` response helper now automatically prepends `<!DOCTYPE html>` to the + body if it is not already present + +## v0.9.0 (2025-11-18) + +- Add `session` middleware for automatic management of `context.session` across + requests + + ```tsx + import { createCookie } from '@remix-run/cookie' + import { createFileStorage } from '@remix-run/session/file-storage' + import { session } from '@remix-run/fetch-router/session-middleware' + + let cookie = createCookie('session', { secrets: ['s3cr3t'] }) + let storage = createFileStorage('/tmp/sessions') + + let router = createRouter({ + middleware: [session(cookie, storage)], + }) + + router.map('/', ({ session }) => { + session.set('count', Number(session.get('count') ?? 0) + 1) + return new Response(`Count: ${session.get('count')}`) + }) + ``` + +- Add `asyncContext` middleware for storing the request context in + `AsyncLocalStorage` so it is available to all functions in the same async + execution context + + ```tsx + import * as assert from 'node:assert/strict' + import { asyncContext } from '@remix-run/fetch-router/async-context-middleware' + + let router = createRouter({ + middleware: [asyncContext()], + }) + + router.map('/', (context) => { + assert.equal(context, getContext()) + return new Response('Home') + }) + ``` + +- Add `file` response helper for serving files + + ```tsx + import * as res from '@remix-run/fetch-router/response-helpers' + import { openFile } from '@remix-run/fs' + + router.get('/assets/:filename', async ({ request, params }) => { + let file = openFile(`./public/assets/${params.filename}`) + return res.file(file, request) + }) + ``` + +- Add `staticFiles` middleware for serving static files + + ```tsx + import { staticFiles } from '@remix-run/fetch-router/static-middleware' + + let router = createRouter({ + middleware: [staticFiles('./public')], + }) + ``` + +## v0.8.0 (2025-11-03) + +- BREAKING CHANGE: Rework how middleware works in the router. This change has + far-reaching implications. + + Previously, the router would associate all middleware with a route. If no + routes matched, middleware would not run. We partially addressed this in 0.7 + by always running global middleware, even when no route matches. However, the + router would still run its route matching algorithm before determining that no + routes matched, so it could proceed to run global middleware and the default + handler. + + In this release, `router.use()` has been replaced with + `createRouter({ middleware })`. Middleware that is provided to + `createRouter()` is "router middleware" (aka "global" middleware) that runs + before the router tries to do any route matching. Router middleware may + therefore modify the request context in ways that may affect route matching, + including modifying `context.method` and/or `context.url`. Router middleware + runs on every request, even when no routes match. + + Middleware is still supported at the route level on individual routes, but it + is only invoked when that route matches. This is "route middleware" (or + "inline" middleware) and runs downstream from router middleware. + + To migrate, move middleware from `router.use()` to + `createRouter({ middleware })`. + + ```tsx + // before + let router = createRouter() + router.use(middleware) + router.map(routes.home, () => new Response('Home')) + + // after + let router = createRouter({ + middleware: [middleware], + }) + router.map(routes.home, () => new Response('Home')) + ``` + +- BREAKING CHANGE: Rename `use` => `middleware` in route handler definitions + + ```tsx + // before + router.map(routes.home, { + use: [middleware], + handler() { + return new Response('Home') + }, + }) + + // after + router.map(routes.home, { + middleware: [middleware], + handler() { + return new Response('Home') + }, + }) + ``` + +- BREAKING CHANGE: Remove `router.mount()` and support for sub-routers. We may + add this back in a future release if there is demand for it. + +- BREAKING CHANGE: Move `FormData` parsing and method override handling out of + the router and into separate middleware exports. Since `methodOverride()` + provides `context.method` (used for route matching), it must be router (or + "global") middleware. Also, it requires `context.formData`, so it must be + after the `formData()` middleware in the middleware chain. This change also + moves the `createRouter({ parseFormData, methodOverride, uploadHandler })` + options to the `formData()` and `methodOverride()` middlewares. + + ```tsx + // before + let router = createRouter({ + parseFormData: true, + methodOverride: true, + uploadHandler, + }) + + // after + import { formData } from '@remix-run/fetch-router/form-data-middleware' + import { methodOverride } from '@remix-run/fetch-router/method-override-middleware' + + let router = createRouter() + router.use(formData({ uploadHandler })) + router.use(methodOverride()) + ``` + + This change makes things a little more verbose but should ultimately lead to + more flexible middleware composition and a smaller core build. + +## v0.7.0 (2025-10-31) + +- BREAKING CHANGE: Move `@remix-run/form-data-parser`, `@remix-run/headers`, and + `@remix-run/route-pattern` to `peerDependencies`. +- BREAKING CHANGE: Rename `InferRouteHandler` => `BuildRouteHandler` and add a + `Method` generic parameter to build a `RouteHandler` type from a string, route + pattern, or route. +- BREAKING CHANGE: Removed support for passing a `Route` object to `redirect()` + response helper. Use `redirect(routes.home.href())` instead. +- BREAKING CHANGE: Move `html()`, `json()`, and `redirect()` response helpers to + `@remix-run/fetch-router/response-helpers` export +- Always run global middleware, even when no route matches +- More precise type inference for `router.get()`, `router.post()`, etc. route + handlers. +- Add support for nesting route maps via object spread syntax + + ```tsx + import { route, resources } from '@remix-run/fetch-router' + + let routes = route({ + brands: { + ...resources('brands', { only: ['index', 'show'] }), + products: resources('brands/:brandId/products', { + only: ['index', 'show'], + }), + }, + }) + + routes.brands.index // Route<'GET', '/brands'> + routes.brands.show // Route<'GET', '/brands/:id'> + routes.brands.products.index // Route<'GET', '/brands/:brandId/products'> + routes.brands.products.show // Route<'GET', '/brands/:brandId/products/:id'> + ``` + +- Add support for `URL` objects in `redirect()` response helper +- Add support for `request.signal` abort, which now short-circuits the + middleware chain. `router.fetch()` will now throw `DOMException` with + `error.name === 'AbortError'` when a request is aborted +- Fix an issue where `Router`'s `fetch` wasn't spec-compliant +- Provide empty `context.formData` to `POST`/`PUT`/etc handlers when + `parseFormData: false` + +## v0.6.0 (2025-10-10) + +- BREAKING CHANGE: Rename + - `resource('...', { routeNames })` to `resource('...', { names })` + - `resources('...', { routeNames })` to `resources('...', { names })` + - `formAction('...', { routeNames })` to `formAction('...', { names })` + - `formAction('...', { submitMethod })` to `formAction('...', { formMethod })` +- Integrate form data handling directly into the router, along with support for + method override and file uploads. The `methodOverride` field overrides the + request method used for matching with the value submitted in the request body. + This makes it possible to use HTML forms to simulate RESTful API request + methods like PUT and DELETE. + + ```tsx + let router = createRouter({ + // Options for parsing form data, or `false` to disable + parseFormData: { + maxFiles: 5, // Maximum number of files that can be uploaded in a single request + maxFileSize: 10 * 1024 * 1024, // 10MB maximum size of each file + maxHeaderSize: 1024 * 1024, // 1MB maximum size of the header + }, + // A function that handles file uploads. It receives a `FileUpload` object and may return any value that is valid in a `FormData` object + uploadHandler(file: FileUpload) { + // save the file to disk/storage... + return '/uploads/file.jpg' + }, + // The name of the form field to check for method override, or `false` to disable + methodOverride: '_method', + }) + ``` + +- Export `InferRouteHandler` and `InferRequestHandler` types +- Re-export `FormDataParseError`, `FileUpload`, and `FileUploadHandler` from + `@remix-run/form-data-parser` +- Fix an issue where per-route middleware was not being applied to a route + handler nested inside a route map with its own middleware + +## v0.5.0 (2025-10-05) + +- Add `formData` middleware for parsing `FormData` objects from the request body + + ```tsx + import { formData } from '@remix-run/fetch-router/form-data-middleware' + + let router = createRouter() + + router.use(formData()) + + router.map('/', ({ formData, files }) => { + console.log(formData) // FormData from the request body + console.log(files) // Record<string, File> from the request body + return new Response('Home') + }) + ``` + +- Add `storage.has(key)` for checking if a value is stored for a given key +- Add `next(moreContext)` API for passing additional context to the next + middleware or handler in the chain +- Move `logger` middleware to `@remix-run/fetch-router/logger-middleware` export +- Add `json` and `redirect` response helpers + + ```tsx + import { json, redirect, createRouter } from '@remix-run/fetch-router' + + let router = createRouter() + + router.map('/api', () => { + return json({ message: 'Hello, world!' }) + }) + + router.map('/*path/', ({ params }) => { + // Strip all trailing slashes from URL paths + return redirect(`/${params.path}`, 301) + }) + ``` + + `redirect` also accepts a `Route` object for type-safe redirects: + + ```tsx + let routes = createRoutes({ + home: '/', + }) + + let response = redirect(routes.home) + ``` + + Note: the route must support `GET` (or `ANY`) for redirects and must not have + any required params, so the helper can safely construct the redirect URL. + +## v0.4.0 (2025-10-04) + +- BREAKING CHANGE: Remove "middleware as an optional 2nd arg" from all router + methods and introduced support for defining middleware inline in route handler + definitions. This greatly reduces the number of overloads required in the + router API and also provides a means whereby middleware may be coupled to + request handler definitions + + ```tsx + // before + router.map('/', [middleware], () => { + return new Response('Home') + }) + + // after + router.map('/', { + use: [middleware], + handler(ctx) { + return new Response('Home') + }, + }) + ``` + +- Add `routeNames` option to `createResource` and `createResources` for + customizing the names of the resource routes. This is a map of the default + route name to a custom name. + + ```tsx + let books = createResources('books', { + routeNames: { index: 'list', show: 'view' }, + }) + + books.list // Route<'GET', '/books'> + books.view // Route<'GET', '/books/:id'> + ``` + +- Add `route` shorthand for `createRoutes` to public exports +- Add support for any `BodyInit` in `html(body)` response helper +- Add `createFormAction` (also exported as `formAction` for short) for creating + route maps with `index` (`GET`) and `action` (`POST`) routes. This is + well-suited to showing a standard HTML `<form>` and handling its submit action + at the same URL. +- Export `RouteHandlers` and `RouteHandler` types + +## v0.3.0 (2025-10-03) + +- Add `router.map()` for registering routes and middleware either one at a time + or in bulk + + One at a time: + + ```tsx + let router = createRouter() + router.map('/', () => new Response('Home')) + router.map('/blog', () => new Response('Blog')) + ``` + + In bulk: + + ```tsx + let routes = createRoutes({ + home: '/', + blog: '/blog', + }) + + let router = createRouter() + + router.map(routes, { + home() { + return new Response('Home') + }, + blog() { + return new Response('Blog') + }, + }) + ``` + +- Add `createResource` and `createResources` functions for creating + resource-based route maps + + ```tsx + import { resource, resources, createRoutes } from '@remix-run/fetch-router' + + let routes = createRoutes({ + home: '/', + books: resources('books'), // Plural resources + profile: resource('profile'), // Singleton resource + }) + + let router = createRouter() + + // Plural resources + router.map(routes.books, { + // GET /books + index() { + return new Response('Books Index') + }, + // POST /books + create() { + return new Response('Book Created', { status: 201 }) + }, + // GET /books/new + new() { + return new Response('New Book') + }, + // GET /books/:id + show({ params }) { + return new Response(`Book ${params.id}`) + }, + // GET /books/:id/edit + edit({ params }) { + return new Response(`Edit Book ${params.id}`) + }, + // PUT /books/:id + update({ params }) { + return new Response(`Updated Book ${params.id}`) + }, + // DELETE /books/:id + destroy({ params }) { + return new Response(`Destroyed Book ${params.id}`) + }, + }) + + // Singleton resource + router.map(routes.profile, { + // GET /profile/:id + show({ params }) { + return new Response(`Profile ${params.id}`) + }, + // GET /profile/new + new() { + return new Response('New Profile') + }, + // POST /profile + create() { + return new Response('Profile Created', { status: 201 }) + }, + // GET /profile/:id/edit + edit({ params }) { + return new Response(`Edit Profile ${params.id}`) + }, + // PUT /profile/:id + update({ params }) { + return new Response(`Updated Profile ${params.id}`) + }, + // DELETE /profile/:id + destroy({ params }) { + return new Response(`Destroyed Profile ${params.id}`) + }, + }) + ``` + +## v0.2.0 (2025-10-02) + +- Add `router.mount(prefix, router)` method for mounting a router at a given + pathname prefix in another router + + ```tsx + let apiRouter = createRouter() + apiRouter.get('/', () => new Response('API')) + + let router = createRouter() + router.mount('/api', apiRouter) + + let response = await router.fetch('https://remix.run/api') + + assert.equal(response.status, 200) + assert.equal(await response.text(), 'API') + ``` + +## v0.1.0 (2025-10-01) + +- Initial release diff --git a/docs/agents/remix/fetch-router/index.md b/docs/agents/remix/fetch-router/index.md index 0b6f0e7..d9eabee 100644 --- a/docs/agents/remix/fetch-router/index.md +++ b/docs/agents/remix/fetch-router/index.md @@ -1,13 +1,12 @@ -# fetch-router - -Source: https://github.com/remix-run/remix/tree/main/packages/fetch-router +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/fetch-router --> -## Overview +# fetch-router 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. +[`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern). +Use it to define typed route maps, run middleware, and share request-scoped +context across APIs, web services, and server-rendered applications. ## Features @@ -15,39 +14,936 @@ server-rendered applications across any JavaScript runtime. 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 +- **Typed Request Context**: Carry request-scoped context through routers, + controllers, and actions +- **Declarative Route Maps**: Define your 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`. +## Usage + +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). + +Import route definition helpers (`route`, `form`, `resource`, `resources`, etc.) +from `remix/fetch-router/routes`, especially in a dedicated `routes.ts` file. +Import runtime APIs (`createRouter`, `Middleware`, etc.) from +`remix/fetch-router`. + +```ts +// routes.ts +import { route, form, resources } from 'remix/fetch-router/routes' + +// router.ts +import { createRouter } from 'remix/fetch-router' +``` + +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. +// Controllers always use the shape: { actions, middleware? }. +router.map(routes, { + actions: { + home() { + return new Response('Home') + }, + about() { + return new Response('About') + }, + blog: { + actions: { + 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 pass 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](#routing-based-on-request-method) in detail +later. But first, let's talk about navigation. + +### 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 `response`](https://github.com/remix-run/remix/tree/main/packages/response#readme) +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(` + <html> + <body> + <h1>Home</h1> + <p> + <a href="${routes.contact.href()}">Contact Us</a> + </p> + </body> + </html> + `) +}) + +// Register an action for `GET /contact` +router.get(routes.contact, () => { + return createHtmlResponse(` + <html> + <body> + <h1>Contact Us</h1> + <form method="POST" action="${routes.contact.href()}"> + <div> + <label for="message">Message</label> + <input type="text" name="message" /> + </div> + <button type="submit">Send</button> + </form> + <footer> + <p> + <a href="${routes.home.href()}">Home</a> + </p> + </footer> + </body> + </html> + `) +}) + +// Register an action for `POST /contact` +router.post(routes.contact, ({ get }) => { + // POST actions can read parsed FormData from request context using FormData + // as the context key after the formData middleware has run. + let formData = get(FormData) + let message = formData.get('message') as string + let body = html` + <html> + <body> + <h1>Thanks!</h1> + <div> + <p>You said: ${message}</p> + </div> + <footer> + <p> + <a href="${routes.home.href()}">Home</a> + </p> + </footer> + </body> + </html> + ` + + return createHtmlResponse(body) +}) +``` + +### 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, { + actions: { + home({ method }) { + assert.equal(method, 'GET') + return new Response('Home') + }, + contact: { + actions: { + index({ method }) { + assert.equal(method, 'GET') + return new Response('Contact') + }, + action({ method }) { + assert.equal(method, 'POST') + return new Response('Contact Action') + }, + }, + }, + }, +}) +``` + +### Declaring Routes + +In addition to the `{ method, pattern }` syntax shown above, the router provides +a few shorthand methods that help 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 + `<form>` and handling its submit action at the same URL. +- [`resources` (and `resource`)](#resource-based-routes) - creates a route map + with a set of resource-based routes, useful when defining RESTful API routes + or + [Rails-style resource-based routes](https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default). + +#### Declaring Form Routes + +Continuing with +[the example of the contact page](#routing-based-on-request-method), let's use +the `form` shorthand to make the route map a little less verbose. + +A `form()` route map contains two routes: `index` and `action`. The `index` +route is a `GET` route that shows the form, and the `action` route is a `POST` +route that handles the form submission. + +```tsx +import { createRouter } from 'remix/fetch-router' +import { route, form } from 'remix/fetch-router/routes' +import { createHtmlResponse } from 'remix/response/html' +import { html } from 'remix/html-template' + +let routes = route({ + home: '/', + contact: form('contact'), +}) + +type Routes = typeof routes +// { +// home: Route<'ANY', '/'> +// contact: { +// index: Route<'GET', '/contact'> - Shows the form +// action: Route<'POST', '/contact'> - Handles the form submission +// }, +// } + +let router = createRouter() + +router.map(routes, { + actions: { + home() { + return createHtmlResponse(` + <html> + <body> + <h1>Home</h1> + <footer> + <p> + <a href="${routes.contact.index.href()}">Contact Us</a> + </p> + </footer> + </body> + </html> + `) + }, + contact: { + actions: { + // GET /contact - shows the form + index() { + return createHtmlResponse(` + <html> + <body> + <h1>Contact Us</h1> + <form method="POST" action="${routes.contact.action.href()}"> + <label for="message">Message</label> + <input type="text" name="message" /> + <button type="submit">Send</button> + </form> + </body> + </html> + `) + }, + // POST /contact - handles the form submission + action({ get }) { + let formData = get(FormData) + let message = formData.get('message') as string + let body = html` + <html> + <body> + <h1>Thanks!</h1> + <p>You said: ${message}</p> + + <p> + Got more to say? + <a href="${routes.contact.index.href()}" + >Send another message</a + > + </p> + </body> + </html> + ` + + return createHtmlResponse(body) + }, + }, + }, + }, +}) +``` + +#### 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](https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default)). +You can think of "resources" as a way to define routes for a collection of +related resources, like products, books, users, etc. + +```ts +import { createRouter } from 'remix/fetch-router' +import { route, resources } 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, { + actions: { + // GET /brands + index() { + return new Response('Brands Index') + }, + // GET /brands/:id + show({ params }) { + return new Response(`Brand ${params.id}`) + }, + products: { + actions: { + // 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 (i.e. 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 { route, resources, resource } 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'> +// }, +// }, +// } +``` + +In both of the examples above we used the `only` option to limit the routes +generated by `resources()`/`resource()` to only the routes we needed. 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 { route, resources } 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 { route, resources } 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 +// } +``` + +### 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. The object form for route actions is `{ handler, middleware? }`, so +you can omit `middleware` entirely when you do not need it. + +```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' })], + handler() { + return new Response('Dashboard') + }, +}) +``` + +### Request Context + +Every action and middleware receives a `context` object with useful properties: + +```ts +const UserKey = createContextKey<{ id: string }>() + +router.get('/posts/:id', (context) => { + // request: The original Request object + console.log(context.request.method) // "GET" + console.log(context.request.headers.get('Accept')) + + // url: Parsed URL object + console.log(context.url.pathname) // "/posts/123" + console.log(context.url.searchParams.get('sort')) + + // params: Route parameters (fully typed!) + console.log(context.params.id) // "123" + + // set/get: type-safe request-scoped context data on the context object + context.set(UserKey, currentUser) + let user = context.get(UserKey) + console.log(user.id) + + return new Response(`Post ${context.params.id}`) +}) +``` + +### Typed Context Contracts + +Route params are only half of a handler's type contract. In many apps, handlers +also depend on values that middleware loads into request context, like sessions, +database connections, or authenticated users. + +`fetch-router` now lets you carry that context contract through the router, +controller, and action types directly. A common pattern is to derive one +app-local context type from your router middleware, then reuse it across stored +controllers and actions. + +```ts +import { Auth, requireAuth, type WithRequiredAuth } from 'remix/auth-middleware' +import { + type BuildAction, + type RequestContext, + type WithParams, +} from 'remix/fetch-router' +import { route } from 'remix/fetch-router/routes' + +let routes = route({ + account: '/account', +}) + +type AppContext<params extends Record<string, string> = {}> = WithParams< + RequestContext, + params +> + +type AuthIdentity = { id: string } + +type AuthenticatedAppContext<params extends Record<string, string> = {}> = + WithRequiredAuth<AppContext<params>, AuthIdentity> + +let accountAction = { + middleware: [requireAuth<AuthIdentity>()], + handler(context) { + let auth = context.get(Auth) + return Response.json({ id: auth.identity.id }) + }, +} satisfies BuildAction<'GET', typeof routes.account, AuthenticatedAppContext> +``` + +In this example, the action declares the stronger context it requires, and the +action-local middleware makes that contract true at runtime. In a larger app, +you can still derive a shared base context from router middleware with +`MiddlewareContext<typeof middleware>` and build on top of it the same way. + +#### Middleware Provider Guidance + +If you're authoring a middleware package that stores values in request context, +treat that context contract as part of the package API. A good provider should +usually export: + +- the context key consumers read with `context.get(...)` +- the middleware that populates that key at runtime +- one or more `With...` helper types (optional) that let applications describe + the resulting request context without touching raw context entries directly + +```ts +import { + createContextKey, + type MergeContext, + type RequestContext, +} from 'remix/fetch-router' + +// The context key that consumers will need to read from `context.get(...)` +export const CurrentUser = createContextKey<User | null>() + +// One or more With* helper types that apps can use to describe the request context +export type WithCurrentUser<context extends RequestContext<any, any>> = + MergeContext<context, [readonly [typeof CurrentUser, User | null]]> +``` + +Built-in middleware packages may also export `With...` helpers when that makes +controller and action contracts clearer, for example `auth-middleware` provides +`WithAuth` and `WithRequiredAuth`. + +### 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/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 `context.get(FormData)` to access parsed form data +- use `context.get(FormData).get(name)`/`getAll(name)` to access 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 `<input name="_method" value="...">` to override the request + method + +### Response Helpers + +Response helpers for creating common HTTP responses are available in the +[`response`](https://github.com/remix-run/remix/tree/main/packages/response) +package: + +```tsx +import { createFileResponse } from 'remix/response/file' +import { createHtmlResponse } from 'remix/response/html' +import { createRedirectResponse } from 'remix/response/redirect' +import { compressResponse } from 'remix/response/compress' + +let response = createHtmlResponse('<h1>Hello</h1>') +let response = Response.json({ message: 'Hello' }) +let response = createRedirectResponse('/') +let response = compressResponse(uncompressedResponse, request) +``` + +See the +[`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 +[`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/html-template' +import { createHtmlResponse } from 'remix/response/html' + +// Use the template tag to escape unsafe variables in HTML. +let unsafe = '<script>alert(1)</script>' +let response = createHtmlResponse(html`<h1>${unsafe}</h1>`, { 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 = '<b>Bold</b>' +let content = html.raw`<div class="content">${safeHtml}</div>` +let response = createHtmlResponse(content) + +// This is particularly useful when building HTML from multiple safe fragments +let header = '<header>Title</header>' +let body = '<main>Content</main>' +let footer = '<footer>Footer</footer>' +let page = html.raw` + <!DOCTYPE html> + <html> + <body> + ${header} + ${body} + ${footer} + </body> + </html> +` + +// You can nest html.raw inside html to preserve SafeHtml fragments +let icon = html.raw`<svg>...</svg>` +let button = html`<button>${icon} Click me</button>` // 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 -## Navigation +## License -- [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) +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) 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 - `<form>` and handling its submit action at the same URL. -- [`resources` (and `resource`)](./routing-resources.md) - creates a route map - with a set of resource-based routes, useful when defining RESTful API routes - or - [Rails-style resource-based routes](https://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default). - -### Declaring form routes - -Continuing with the contact page example, let's use the `form` shorthand to make -the route map a little less verbose. - -A `form()` route map contains two routes: `index` and `action`. The `index` -route is a `GET` route that shows the form, and the `action` route is a `POST` -route that handles the form submission. - -```tsx -import { createRouter } from 'remix/fetch-router' -import { form, route } from 'remix/fetch-router/routes' -import { createHtmlResponse } from 'remix/response/html' -import { html } from 'remix/html-template' - -let routes = route({ - home: '/', - contact: form('contact'), -}) - -type Routes = typeof routes -// { -// home: Route<'ANY', '/'> -// contact: { -// index: Route<'GET', '/contact'> - Shows the form -// action: Route<'POST', '/contact'> - Handles the form submission -// }, -// } - -let router = createRouter() - -router.map(routes, { - home() { - return createHtmlResponse(` - <html> - <body> - <h1>Home</h1> - <footer> - <p> - <a href="${routes.contact.index.href()}">Contact Us</a> - </p> - </footer> - </body> - </html> - `) - }, - contact: { - // GET /contact - shows the form - index() { - return createHtmlResponse(` - <html> - <body> - <h1>Contact Us</h1> - <form method="POST" action="${routes.contact.action.href()}"> - <label for="message">Message</label> - <input type="text" name="message" /> - <button type="submit">Send</button> - </form> - </body> - </html> - `) - }, - // POST /contact - handles the form submission - action({ formData }) { - let message = formData.get('message') as string - let body = html` - <html> - <body> - <h1>Thanks!</h1> - <p>You said: ${message}</p> - - <p> - Got more to say? - <a href="${routes.contact.index.href()}">Send another message</a> - </p> - </body> - </html> - ` - - 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(` - <html> - <body> - <h1>Home</h1> - <p> - <a href="${routes.contact.href()}">Contact Us</a> - </p> - </body> - </html> - `) -}) - -// Register an action for `GET /contact` -router.get(routes.contact, () => { - return createHtmlResponse(` - <html> - <body> - <h1>Contact Us</h1> - <form method="POST" action="${routes.contact.href()}"> - <div> - <label for="message">Message</label> - <input type="text" name="message" /> - </div> - <button type="submit">Send</button> - </form> - <footer> - <p> - <a href="${routes.home.href()}">Home</a> - </p> - </footer> - </body> - </html> - `) -}) - -// 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` - <html> - <body> - <h1>Thanks!</h1> - <div> - <p>You said: ${message}</p> - </div> - <footer> - <p> - <a href="${routes.home.href()}">Home</a> - </p> - </footer> - </body> - </html> - ` - - 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/changelog.md b/docs/agents/remix/file-storage-s3/changelog.md new file mode 100644 index 0000000..006f983 --- /dev/null +++ b/docs/agents/remix/file-storage-s3/changelog.md @@ -0,0 +1,31 @@ +# `file-storage-s3` CHANGELOG + +This is the changelog for +[`file-storage-s3`](https://github.com/remix-run/remix/tree/main/packages/file-storage-s3). +It follows [semantic versioning](https://semver.org/). + +## v0.1.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`file-storage@0.13.4`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.4) + +## v0.1.0 + +### Minor Changes + +- #### Unreleased + + Initial release of `@remix-run/file-storage-s3`. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`file-storage@0.13.3`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.3) + +## Unreleased + +### Minor Changes + +- Initial release of `@remix-run/file-storage-s3`. diff --git a/docs/agents/remix/file-storage-s3/index.md b/docs/agents/remix/file-storage-s3/index.md new file mode 100644 index 0000000..52bf5ba --- /dev/null +++ b/docs/agents/remix/file-storage-s3/index.md @@ -0,0 +1,57 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/file-storage-s3 --> + +# 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/changelog.md b/docs/agents/remix/file-storage/changelog.md new file mode 100644 index 0000000..fbece29 --- /dev/null +++ b/docs/agents/remix/file-storage/changelog.md @@ -0,0 +1,197 @@ +# `file-storage` CHANGELOG + +This is the changelog for +[`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage). +It follows [semantic versioning](https://semver.org/). + +## v0.13.4 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fs@0.4.3`](https://github.com/remix-run/remix/releases/tag/fs@0.4.3) + - [`lazy-file@5.0.3`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.3) + +## v0.13.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fs@0.4.2`](https://github.com/remix-run/remix/releases/tag/fs@0.4.2) + - [`lazy-file@5.0.2`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.2) + +## v0.13.2 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.13.1 + +### Patch Changes + +- Update `@remix-run/fs` peer dependency to use new `openLazyFile()` API + +## v0.13.0 (2025-11-25) + +- BREAKING CHANGE: `LocalFileStorage` class has been replaced with + `createFsFileStorage(directory)` +- BREAKING CHANGE: `MemoryFileStorage` class has been replaced with + `createMemoryFileStorage()` + + ```ts + // before + import { LocalFileStorage } from '@remix-run/file-storage/local' + import { MemoryFileStorage } from '@remix-run/file-storage/memory' + let fsStorage = new LocalFileStorage('./files') + let memoryStorage = new MemoryFileStorage() + + // after + import { createFsFileStorage } from '@remix-run/file-storage/fs' + import { createMemoryFileStorage } from '@remix-run/file-storage/memory' + let fsStorage = createFsFileStorage('./files') + let memoryStorage = createMemoryFileStorage() + ``` + +## v0.12.0 (2025-11-20) + +- Add `@remix-run/fs` as a peer dependency. This package now imports from + `@remix-run/fs` instead of `@remix-run/lazy-file/fs`. + +## v0.11.0 (2025-11-05) + +- Move `@remix-run/lazy-file` to `peerDependencies` +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.10.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v0.9.0 (2025-07-25) + +- Remove hash directories when they are empty in `LocalFileStorage` + +## v0.8.0 (2025-07-21) + +- Renamed package from `@mjackson/file-storage` to `@remix-run/file-storage` + +## v0.7.0 (2025-06-10) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v0.6.1 (2025-02-06) + +- Fix regression when using `LocalFileStorage` together with `form-data-parser` + (see #53) + +## v0.6.0 (2025-02-04) + +- BREAKING CHANGE: `LocalFileStorage` now uses 2 characters for shard directory + names instead of 8. +- Buffer contents of files stored in `MemoryFileStorage`. +- Add `storage.list(options)` for listing files in storage. + +The following `options` are available: + +- `cursor`: An opaque string that allows you to paginate over the keys in + storage +- `includeMetadata`: If `true`, include file metadata in the result +- `limit`: The maximum number of files to return +- `prefix`: Only return keys that start with this string + +For example, to list all files under keys that start with `user123/`: + +```ts +let result = await storage.list({ prefix: 'user123/' }) +console.log(result.files) +// [ +// { key: "user123/..." }, +// { key: "user123/..." }, +// ... +// ] +``` + +`result.files` will be an array of `{ key: string }` objects. To include +metadata about each file, use `includeMetadata: true`. + +```ts +let result = await storage.list({ prefix: 'user123/', includeMetadata: true }) +console.log(result.files) +// [ +// { +// key: "user123/...", +// lastModified: 1737955705270, +// name: "hello.txt", +// size: 16, +// type: "text/plain" +// }, +// ... +// ] +``` + +Pagination is done via an opaque `cursor` property in the list result object. If +it is not `undefined`, there are more files to list. You can list them by +passing the `cursor` back in the `options` object on the next call. For example, +to list all items in storage, you could do something like this: + +```ts +let result = await storage.list() +console.log(result.files) + +while (result.cursor !== undefined) { + result = await storage.list({ cursor: result.cursor }) + console.log(result.files) +} +``` + +Use the `limit` option to limit how many results you get back in the `files` +array. + +## v0.5.0 (2025-01-25) + +- Add `storage.put(key, file)` method as a convenience around + `storage.set(key, file)` + `storage.get(key)`, which is a very common pattern + when you need immediate access to the file you just put in storage + +```ts +// before +await storage.set(key, file) +let newFile = await storage.get(key)! + +// after +let newFile = await storage.put(key, file) +``` + +## v0.4.1 (2025-01-10) + +- Fix missing types for `file-storage/local` in npm package + +## v0.4.0 (2025-01-08) + +- Fixes race conditions with concurrent calls to `set` +- Shards storage directories for more scalable file systems + +## v0.3.0 (2024-11-14) + +- Added CommonJS build +- Upgrade to lazy-file@3.1.0 + +## v0.2.1 (2024-09-04) + +- Automatically clean up old files in `LocalFileStorage` when new files are + stored with the same key + +## v0.2.0 (2024-08-26) + +- Moved `LocalFileStorage` to `file-storage/local` export +- Moved `MemoryFileStorage` to `file-storage/memory` export + +## v0.1.0 (2024-08-24) + +- Initial release diff --git a/docs/agents/remix/file-storage.md b/docs/agents/remix/file-storage/index.md similarity index 62% rename from docs/agents/remix/file-storage.md rename to docs/agents/remix/file-storage/index.md index 4cf3af1..68b6840 100644 --- a/docs/agents/remix/file-storage.md +++ b/docs/agents/remix/file-storage/index.md @@ -1,39 +1,24 @@ -# 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. +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/file-storage --> -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. +# file-storage -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 +62,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/changelog.md b/docs/agents/remix/form-data-middleware/changelog.md new file mode 100644 index 0000000..1016f58 --- /dev/null +++ b/docs/agents/remix/form-data-middleware/changelog.md @@ -0,0 +1,79 @@ +# `form-data-middleware` CHANGELOG + +This is the changelog for +[`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.2.2 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`form-data-parser@0.17.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.17.0) + +## v0.2.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.2.0 + +### Minor Changes + +- BREAKING CHANGE: Form data middleware no longer reads/writes + `context.formData` or `context.files`. + + Parsed `FormData` is now stored on request context with + `context.set(FormData, formData)` and should be read with + `context.get(FormData)`, including uploaded files via + `get(...)`/`getAll(...)`. + +- `formData()` now contributes `FormData` to `fetch-router`'s typed request + context, so apps deriving context from middleware can read + `context.get(FormData)` without manual type assertions. + +### Patch Changes + +- `formData()` is now a no-op if `FormData` has already been parsed earlier in + the request pipeline, so the middleware can be registered multiple times + without re-reading or re-parsing the request body. + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + - [`form-data-parser@0.16.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.16.0) + +## v0.1.4 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + +## v0.1.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + - [`@remix-run/form-data-parser@0.15.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.15.0) + +## v0.1.2 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.1.1 (2025-12-06) + +- Explicitly set `context.formData` in all `POST` cases, even when the request + body is invalid + +## v0.1.0 (2025-11-19) + +Initial release extracted from `@remix-run/fetch-router` v0.9.0. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/form-data-middleware/README.md) +for more details. diff --git a/docs/agents/remix/form-data-middleware/index.md b/docs/agents/remix/form-data-middleware/index.md new file mode 100644 index 0000000..380119b --- /dev/null +++ b/docs/agents/remix/form-data-middleware/index.md @@ -0,0 +1,123 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/form-data-middleware --> + +# 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/changelog.md b/docs/agents/remix/form-data-parser/changelog.md new file mode 100644 index 0000000..33636a3 --- /dev/null +++ b/docs/agents/remix/form-data-parser/changelog.md @@ -0,0 +1,171 @@ +# `form-data-parser` CHANGELOG + +This is the changelog for +[`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser). +It follows [semantic versioning](https://semver.org/). + +## v0.17.0 + +### Minor Changes + +- BREAKING CHANGE: Errors thrown or rejected by a `parseFormData()` upload + handler now propagate directly instead of being wrapped in a + `FormDataParseError`. + +### Patch Changes + +- Preserve non-ASCII multipart field names and filenames when parsing + `multipart/form-data` requests. + +- Bumped `@remix-run/*` dependencies: + - [`multipart-parser@0.16.0`](https://github.com/remix-run/remix/releases/tag/multipart-parser@0.16.0) + +## v0.16.0 + +### Minor Changes + +- BREAKING CHANGE: `parseFormData()` now enforces finite default multipart + `maxParts` and `maxTotalSize` limits and surfaces multipart limit failures + directly instead of treating them as generic parse noise. + + Apps that intentionally accept large multipart submissions may need to raise + these limits explicitly. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`multipart-parser@0.15.0`](https://github.com/remix-run/remix/releases/tag/multipart-parser@0.15.0) + +## v0.15.0 + +### Minor Changes + +- Bump multipart-parser dependency to 0.14.2 + +## v0.14.0 (2025-11-05) + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.13.0 (2025-11-04) + +- Throw `FormDataParseError` when the request body is malformed + multipart/form-data. The underlying `MultipartParseError` is its `cause`. + +## v0.12.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v0.11.0 (2025-10-05) + +- Make `options` optional in `parseFormData` signature +- Export `ParseFormDataOptions` type + +## v0.10.1 (2025-07-24) + +- Update to `@remix-run/multipart-parser` v0.11.0 + +## v0.10.0 (2025-07-24) + +- Renamed package from `@mjackson/form-data-parser` to + `@remix-run/form-data-parser` + +## v0.9.1 (2025-06-13) + +- Export `FormDataParseError` and `MaxFilesExceededError` +- Re-export `MultipartParseError`, `MaxHeaderSizeExceededError`, and + `MaxFileSizeExceededError` from multipart parser + +## v0.9.0 (2025-06-13) + +This release updates to `multipart-parser` 0.10.0 and removes the restrictions +on checking the `size` and/or `slice`ing `FileUpload` objects. + +- `FileUpload` is now a normal subclass of `File` with all the same + functionality (instead of just implementing the same interface) +- Add `maxFiles` option to `parseFormData` to allow limiting the number of files + uploaded in a single request +- BREAKING CHANGE: `parseFormData()` now defaults to `maxFiles = 20`; set + `maxFiles` explicitly to allow larger batch uploads. + +```ts +let formData = await parseFormData(request, { maxFiles: 5 }) +let file = formData.get('file-upload') +let size = file.size // This is ok now! +``` + +## v0.8.0 (2025-06-10) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v0.7.0 (2025-01-25) + +- BREAKING CHANGE: Override `parseFormData` signature so the upload handler is + always last in the argument list. `parserOptions` are now an optional 2nd arg. + +```ts +import { parseFormData } from '@remix-run/form-data-parser' + +// before +await parseFormData( + request, + (fileUpload) => { + // ... + }, + { maxFileSize }, +) + +// after +await parseFormData(request, { maxFileSize }, (fileUpload) => { + // ... +}) +``` + +- Upgrade + [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) + to v0.8 to fix an issue where errors would crash the process when + `maxFileSize` was exceeded (see #28) +- Add a + [demo of how to use `form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos/node) + together with + [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) + to handle multipart uploads on Node.js +- Expand `FileUploadHandler` interface to support returning `Blob` from the + upload handler, which is the superclass of `File` + +## v0.6.0 (2025-01-15) + +- Allow upload handlers to run in parallel. Fixes #44 + +## v0.5.1 (2024-12-12) + +- Fix dependency on `headers` in package.json + +## v0.5.0 (2024-11-14) + +- Added CommonJS build + +## v0.4.0 (2024-09-05) + +- Allow passing `MultipartParserOptions` as optional 3rd arg to + `parseFormData()` + +## v0.3.0 (2024-09-05) + +- Make `FileUpload` implement the `File` interface instead of extending `File` + (fixes https://github.com/mjackson/form-data-parser/issues/4) +- Allow returning `null` from an upload handler, so it allows + `return fileStorage.get(key)` without type errors + +## v0.2.0 (2024-08-28) + +- Add missing `FileUpload` export 🤦‍♂️ + +## v0.1.0 (2024-08-24) + +- Initial release diff --git a/docs/agents/remix/form-data-parser.md b/docs/agents/remix/form-data-parser/index.md similarity index 72% rename from docs/agents/remix/form-data-parser.md rename to docs/agents/remix/form-data-parser/index.md index f80095d..50b7f26 100644 --- a/docs/agents/remix/form-data-parser.md +++ b/docs/agents/remix/form-data-parser/index.md @@ -1,8 +1,6 @@ -# form-data-parser - -Source: https://github.com/remix-run/remix/tree/main/packages/form-data-parser +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/form-data-parser --> -## README +# form-data-parser A streaming `multipart/form-data` parser that solves memory issues with file uploads in server environments. Built as an enhanced replacement for the native @@ -49,10 +47,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 +58,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 +94,28 @@ 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`. + +Known limit errors are thrown directly so you can handle them with `instanceof` +checks. Other failures while parsing the request body are wrapped in +`FormDataParseError`, with the original error available as `error.cause`. Errors +thrown or rejected by your `uploadHandler` are not wrapped. ```ts import { + FormDataParseError, MaxFilesExceededError, MaxFileSizeExceededError, -} from '@remix-run/form-data-parser' + MaxHeaderSizeExceededError, + MaxPartsExceededError, + MaxTotalSizeExceededError, +} from 'remix/form-data-parser' const oneKb = 1024 const oneMb = 1024 * oneKb @@ -115,14 +124,24 @@ 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 if (error instanceof FormDataParseError) { + console.error(`Could not parse form data:`, error.cause ?? error) } else { - console.error(`An unknown error occurred:`, error) + throw error } } ``` @@ -133,9 +152,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 +187,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 +199,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/changelog.md b/docs/agents/remix/fs/changelog.md new file mode 100644 index 0000000..a263933 --- /dev/null +++ b/docs/agents/remix/fs/changelog.md @@ -0,0 +1,72 @@ +# `fs` CHANGELOG + +This is the changelog for +[`fs`](https://github.com/remix-run/remix/tree/main/packages/fs). It follows +[semantic versioning](https://semver.org/). + +## v0.4.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`lazy-file@5.0.3`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.3) + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + +## v0.4.2 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`lazy-file@5.0.2`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.2) + - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0) + +## v0.4.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.4.0 + +### Minor Changes + +- BREAKING CHANGE: Renamed `openFile()` to `openLazyFile()`, removed `getFile()` + + Since `LazyFile` no longer extends `File`, the function name now explicitly + reflects the return type. The `getFile()` alias has also been removed—use + `openLazyFile()` instead. + + **Migration:** + + ```ts + import { openLazyFile } from '@remix-run/fs' + + let lazyFile = openLazyFile('./document.pdf') + + // Streaming + let response = new Response(lazyFile.stream()) + + // For non-streaming APIs that require a complete File (e.g. FormData) + formData.append('file', await lazyFile.toFile()) + ``` + + **Note:** `.toFile()` and `.toBlob()` read the entire file into memory. Only + use these for non-streaming APIs that require a complete `File` or `Blob` + (e.g. `FormData`). Always prefer `.stream()` if possible. + +## v0.3.0 (2025-11-26) + +- Move `@remix-run/lazy-file` and `@remix-run/mime` to `peerDependencies` + +## v0.2.0 (2025-11-25) + +- Replaced `mrmime` dependency with `@remix-run/mime` for MIME type detection + +## v0.1.0 (2025-11-20) + +Initial release with filesystem utilities extracted from +`@remix-run/lazy-file/fs`. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/fs/README.md) for +more details. diff --git a/docs/agents/remix/fs.md b/docs/agents/remix/fs/index.md similarity index 79% rename from docs/agents/remix/fs.md rename to docs/agents/remix/fs/index.md index 8e5c38b..cef6195 100644 --- a/docs/agents/remix/fs.md +++ b/docs/agents/remix/fs/index.md @@ -1,13 +1,9 @@ -# fs - -Source: https://github.com/remix-run/remix/tree/main/packages/fs +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/fs --> -## README - -Lazy, streaming filesystem utilities for JavaScript. +# fs -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 +20,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 +29,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 +48,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 +71,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/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<mediaType, quality>`. - -```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<encoding, quality>`. - -```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<language, quality>`. - -```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/changelog.md b/docs/agents/remix/headers/changelog.md new file mode 100644 index 0000000..40318d9 --- /dev/null +++ b/docs/agents/remix/headers/changelog.md @@ -0,0 +1,461 @@ +# `headers` CHANGELOG + +This is the changelog for +[`headers`](https://github.com/remix-run/remix/tree/main/packages/headers). It +follows [semantic versioning](https://semver.org/). + +## v0.19.0 + +### Minor Changes + +- BREAKING CHANGE: Removed `Headers`/`SuperHeaders` class and default export. + Use the native `Headers` class with the static `from()` method on each header + class instead. + + New individual header `.from()` methods: + - `Accept.from()` + - `AcceptEncoding.from()` + - `AcceptLanguage.from()` + - `CacheControl.from()` + - `ContentDisposition.from()` + - `ContentRange.from()` + - `ContentType.from()` + - `Cookie.from()` + - `IfMatch.from()` + - `IfNoneMatch.from()` + - `IfRange.from()` + - `Range.from()` + - `SetCookie.from()` + - `Vary.from()` + + New raw header utilities added: + - `parse()` + - `stringify()` + + Migration example: + + ```ts + // Before: + import SuperHeaders from '@remix-run/headers' + let headers = new SuperHeaders(request.headers) + let mediaType = headers.contentType.mediaType + + // After: + import { ContentType } from '@remix-run/headers' + let contentType = ContentType.from(request.headers.get('content-type')) + let mediaType = contentType.mediaType + ``` + + If you were using the `Headers` constructor to parse raw HTTP header strings, + use `parse()` instead: + + ```ts + // Before: + import SuperHeaders from '@remix-run/headers' + let headers = new SuperHeaders( + 'Content-Type: text/html\r\nCache-Control: no-cache', + ) + + // After: + import { parse } from '@remix-run/headers' + let headers = parse('Content-Type: text/html\r\nCache-Control: no-cache') + ``` + + If you were using `headers.toString()` to convert headers to raw format, use + `stringify()` instead: + + ```ts + // Before: + import SuperHeaders from '@remix-run/headers' + let headers = new SuperHeaders() + headers.set('Content-Type', 'text/html') + let rawHeaders = headers.toString() + + // After: + import { stringify } from '@remix-run/headers' + let headers = new Headers() + headers.set('Content-Type', 'text/html') + let rawHeaders = stringify(headers) + ``` + +## v0.18.0 (2025-11-25) + +- Add `Vary` support + +```ts +import { Vary } from '@remix-run/headers' + +let header = new Vary('Accept-Encoding') +header.add('Accept-Language') +header.headerNames // ['accept-encoding', 'accept-language'] +header.toString() // 'accept-encoding, accept-language' +``` + +- `Accept.getPreferred()`, `AcceptEncoding.getPreferred()`, and + `AcceptLanguage.getPreferred()` are now generic, preserving the union type of + the input array in the return type + +## v0.17.2 (2025-11-25) + +- Fix `secure` property type in `SetCookie` to accept `boolean` instead of only + `true`, making it consistent with `httpOnly` and `partitioned` + +## v0.17.1 (2025-11-21) + +- Fix bug where `Max-Age=0` did not show up in `SetCookie` header + +## v0.17.0 (2025-11-18) + +- Add `Range` support + +```ts +import { Range } from '@remix-run/headers' + +let header = new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }) +header.toString() // "bytes=0-999" + +// Parse from string +let header = new Range('bytes=0-999,2000-2999') +header.ranges // [{ start: 0, end: 999 }, { start: 2000, end: 2999 }] + +// Check if range is satisfiable for a given file size +header.isSatisfiable(5000) // true + +// Normalize ranges to concrete start/end values for a given file size +let header = new Range('bytes=0-') +header.normalize(5000) // [{ start: 0, end: 4999 }] +``` + +- Add `Content-Range` support + +```ts +import { ContentRange } from '@remix-run/headers' + +let header = new ContentRange({ + unit: 'bytes', + start: 0, + end: 999, + size: 5000, +}) +header.toString() // "bytes 0-999/5000" + +// Parse from string +let header = new ContentRange('bytes 200-1000/67589') +header.start // 200 +header.end // 1000 +header.size // 67589 +``` + +- Add `If-Match` support + +```ts +import { IfMatch } from '@remix-run/headers' + +let header = new IfMatch(['"abc123"', '"def456"']) +header.has('"abc123"') // true + +// Check if precondition passes +header.matches('"abc123"') // true +header.matches('"xyz789"') // false +header.matches('W/"abc123"') // false (weak ETags never match) +``` + +- Add `If-Range` support + +```ts +import { IfRange } from '@remix-run/headers' + +// With ETag +let header = new IfRange('"abc123"') +header.matches({ etag: '"abc123"' }) // true +header.matches({ etag: 'W/"abc123"' }) // false (weak ETags never match) + +// With Last-Modified date +let header = new IfRange(new Date('2025-10-21T07:28:00Z')) +header.matches({ lastModified: new Date('2025-10-21T07:28:00Z') }) // true +``` + +- Add `Allow` support + +```ts +import { SuperHeaders } from '@remix-run/headers' + +let headers = new SuperHeaders({ allow: ['GET', 'POST', 'OPTIONS'] }) +headers.get('Allow') // "GET, POST, OPTIONS" +``` + +## v0.16.0 (2025-11-05) + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.15.0 (2025-11-04) + +- Add support for `httpOnly: false` in `SetCookie` constructor +- Export `CookieProperties` type with all cookie properties +- Add `Partitioned` support to `SetCookie` + +## v0.14.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v0.13.0 (2025-10-04) + +- Drop support for TypeScript < 5.7 + +## v0.12.0 (2025-07-18) + +- Rename package from `@mjackson/headers` to `@remix-run/headers` + +## v0.11.1 (2025-06-06) + +- Do not minify builds +- Remove some test files from the build + +## v0.11.0 (2025-06-06) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v0.10.0 (2025-01-27) + +This release contains several improvements to `Cookie` that bring it more in +line with other headers like `Accept`, `AcceptEncoding`, and `AcceptLanguage`. + +- BREAKING CHANGE: `cookie.names()` and `cookie.values()` are now getters that + return `string[]` instead of methods that return `IterableIterator<string>` +- BREAKING CHANGE: `cookie.forEach()` calls its callback with + `(name, value, cookie)` instead of `(value, name, map)` +- BREAKING CHANGE: `cookie.delete(name)` returns `void` instead of `boolean` + +```ts +// before +let cookieNames = Array.from(headers.cookie.names()) + +// after +let cookieNames = headers.cookie.names +``` + +Additionally, this release adds support for the `If-None-Match` header. This is +useful for conditional GET requests where you want to return a response with +content only if the ETag has changed. + +```ts +import { SuperHeaders } from '@remix-run/headers' + +function requestHandler(request: Request): Promise<Response> { + let response = await callDownstreamService(request) + + if (request.method === 'GET' && response.headers.has('ETag')) { + let headers = new SuperHeaders(request.headers) + if (headers.ifNoneMatch.matches(response.headers.get('ETag'))) { + return new Response(null, { status: 304 }) + } + } + + return response +} +``` + +## v0.9.0 (2024-12-20) + +This release tightens up the type safety and brings `SuperHeaders` more in line +with the built-in `Headers` interface. + +- BREAKING CHANGE: The mutation methods `headers.set()` and `headers.append()` + no longer accept anything other than a string as the 2nd arg. This follows the + native `Headers` interface more closely. + +```ts +// before +let headers = new SuperHeaders() +headers.set('Content-Type', { mediaType: 'text/html' }) + +// after +headers.set('Content-Type', 'text/html') + +// if you need the previous behavior, use the setter instead of set() +headers.contentType = { mediaType: 'text/html' } +``` + +Similarly, the constructor no longer accepts non-string values in an array init +value. + +```ts +// before +let headers = new SuperHeaders([['Content-Type', { mediaType: 'text/html' }]]) + +// if you need the previous behavior, use the object init instead +let headers = new SuperHeaders({ contentType: { mediaType: 'text/html' } }) +``` + +- BREAKING CHANGE: `headers.get()` returns `null` for uninitialized custom + header values instead of `undefined`. This follows the native `Headers` + interface more closely. + +```ts +// before +let headers = new SuperHeaders() +headers.get('Host') // null +headers.get('Content-Type') // undefined + +// after +headers.get('Host') // null +headers.get('Content-Type') // null +``` + +- BREAKING CHANGE: Removed ability to initialize `AcceptLanguage` with + `undefined` weight values. + +```ts +// before +let h1 = new AcceptLanguage({ 'en-US': undefined }) +let h2 = new AcceptLanguage([['en-US', undefined]]) + +// after +let h3 = new AcceptLanguage({ 'en-US': 1 }) +``` + +- All setters now also accept `undefined | null` in addition to `string` and + custom object values. Setting a header to `undefined | null` is the same as + using `headers.delete()`. + +```ts +let headers = new SuperHeaders({ contentType: 'text/html' }) +headers.get('Content-Type') // 'text/html' + +headers.contentType = null // same as headers.delete('Content-Type'); +headers.get('Content-Type') // null +``` + +- Allow setting date headers (`date`, `expires`, `ifModifiedSince`, + `ifUnmodifiedSince`, and `lastModified`) using numbers. + +```ts +let ms = new Date().getTime() +let headers = new SuperHeaders({ lastModified: ms }) +headers.date = ms +``` + +- Added `AcceptLanguage.prototype.accepts(language)`, + `AcceptLanguage.prototype.getWeight(language)`, + `AcceptLanguage.prototype.getPreferred(languages)` + +```ts +import { AcceptLanguage } from '@remix-run/headers' + +let header = new AcceptLanguage({ 'en-US': 1, en: 0.9 }) + +header.accepts('en-US') // true +header.accepts('en-GB') // true +header.accepts('en') // true +header.accepts('fr') // false + +header.getWeight('en-US') // 1 +header.getWeight('en-GB') // 0.9 + +header.getPreferred(['en-GB', 'en-US']) // 'en-US' +``` + +- Added `Accept` support + +```ts +import { Accept } from '@remix-run/headers' + +let header = new Accept({ 'text/html': 1, 'text/*': 0.9 }) + +header.accepts('text/html') // true +header.accepts('text/plain') // true +header.accepts('text/*') // true +header.accepts('image/jpeg') // false + +header.getWeight('text/html') // 1 +header.getWeight('text/plain') // 0.9 + +header.getPreferred(['text/html', 'text/plain']) // 'text/html' +``` + +- Added `Accept-Encoding` support + +```ts +import { AcceptEncoding } from '@remix-run/headers' + +let header = new AcceptEncoding({ gzip: 1, deflate: 0.9 }) + +header.accepts('gzip') // true +header.accepts('deflate') // true +header.accepts('identity') // true +header.accepts('br') // false + +header.getWeight('gzip') // 1 +header.getWeight('deflate') // 0.9 + +header.getPreferred(['gzip', 'deflate']) // 'gzip' +``` + +- Added `SuperHeaders.prototype` (getters and setters) for: + - `accept` + - `acceptEncoding` + - `acceptRanges` + - `connection` + - `contentEncoding` + - `contentLanguage` + - `etag` + - `host` + - `location` + - `referer` + +## v0.8.0 (2024-11-14) + +- Added CommonJS build + +## 0.7.2 (2024-08-29) + +- Treat `Headers` as iterable in the constructor + +## v0.7.1 (2024-08-28) + +- Added `string` init type to `new Headers({ acceptLanguage })` + +## v0.7.0 (2024-08-27) + +- Added support for the `Accept-Language` header + (https://github.com/remix-run/remix/pull/8, thanks + [@ArnoSaine](https://github.com/ArnoSaine)) + +## v0.6.1 (2024-08-13) + +- Associate `CacheControl` doc comments with the class instead of the + constructor function + +## v0.6.0 (2024-08-13) + +- Added support for `Cache-Control` header + (https://github.com/mjackson/headers/pull/7, thanks + [@alexanderson1993](https://github.com/alexanderson1993)) + +## v0.5.1 (2024-08-6) + +- Added `CookieInit` support to `headers.cookie=` setter + +## v0.5.0 (2024-08-6) + +- Added the ability to initialize a `SuperHeaders` instance with object config + instead of just strings or header object instances. + +```ts +let headers = new Headers({ + contentType: { mediaType: 'text/html' }, + cookies: [ + ['session_id', 'abc'], + ['theme', 'dark'], + ], +}) +``` + +- Changed package name from `fetch-super-headers` to `@remix-run/headers`. + Eventual goal is to get the `headers` npm package name. 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<etag>`. - -```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<etag>`. - -```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<headerName>`. - -```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<name, value>`. - -```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 index ad5586d..ae157bb 100644 --- a/docs/agents/remix/headers/index.md +++ b/docs/agents/remix/headers/index.md @@ -1,30 +1,620 @@ -# headers +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/headers --> -Source: https://github.com/remix-run/remix/tree/main/packages/headers +# headers -## Overview +Typed utilities for parsing, manipulating, and serializing HTTP header values. +`headers` provides focused classes for common HTTP headers. -Utilities for parsing, manipulating and stringifying HTTP header values. +## Features -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. +- **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 -bun add @remix-run/headers +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](./index.md#accept) +- [Accept-Encoding](./index.md#accept-encoding) +- [Accept-Language](./index.md#accept-language) +- [Cache-Control](./index.md#cache-control) +- [Content-Disposition](./index.md#content-disposition) +- [Content-Range](./index.md#content-range) +- [Content-Type](./index.md#content-type) +- [Cookie](./index.md#cookie) +- [If-Match](./index.md#if-match) +- [If-None-Match](./index.md#if-none-match) +- [If-Range](./index.md#if-range) +- [Range](./index.md#range) +- [Set-Cookie](./index.md#set-cookie) +- [Vary](./index.md#vary) + +### Accept + +Parse, manipulate and stringify +[`Accept` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept). + +Implements `Map<mediaType, quality>`. + +```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<encoding, quality>`. + +```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<language, quality>`. + +```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<name, value>`. + +```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<etag>`. + +```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<etag>`. + +```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<headerName>`. + +```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' ``` -## Header utilities +## Related Packages -- 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) +- [`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 -## Navigation +## License -- [Remix package index](../index.md) +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) 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/changelog.md b/docs/agents/remix/html-template/changelog.md new file mode 100644 index 0000000..c023758 --- /dev/null +++ b/docs/agents/remix/html-template/changelog.md @@ -0,0 +1,26 @@ +# `html-template` CHANGELOG + +This is the changelog for +[`html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template). +It follows [semantic versioning](https://semver.org/). + +## v0.3.0 (2025-11-05) + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.2.0 (2025-10-31) + +- No real changes, just testing a new release process. + +## 0.1.0 (2025-10-25) + +This is the initial release of the `@remix-run/html-template` package. + +- `html` tagged template function for HTML string construction with automatic + escaping +- `html.raw` for explicitly marking HTML as safe (no escaping) +- `isSafeHtml` type guard function +- `SafeHtml` branded type for type-safe HTML strings +- Support for composable HTML fragments without double-escaping +- Support for arrays, primitives, and falsy values in interpolations diff --git a/docs/agents/remix/html-template.md b/docs/agents/remix/html-template/index.md similarity index 69% rename from docs/agents/remix/html-template.md rename to docs/agents/remix/html-template/index.md index b9cd9f7..676c83b 100644 --- a/docs/agents/remix/html-template.md +++ b/docs/agents/remix/html-template/index.md @@ -1,19 +1,10 @@ -# html-template - -Source: https://github.com/remix-run/remix/tree/main/packages/html-template - -## README - -Safe HTML template tag with auto-escaping for JavaScript. +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/html-template --> -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 -`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 +19,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 = '<script>alert("XSS")</script>' let greeting = html`<h1>Hello ${userInput}!</h1>` @@ -52,7 +41,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 = '<svg>...</svg>' let button = html.raw`<button>${trustedIcon} Click me</button>` @@ -69,7 +58,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`<h1>My Title</h1>` let content = html`<p>Some content with ${userInput}</p>` @@ -89,7 +78,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 +93,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 +110,3 @@ let page = html`<div> ## 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..cf4b15e 100644 --- a/docs/agents/remix/index.md +++ b/docs/agents/remix/index.md @@ -1,183 +1,212 @@ # 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) +Downloaded from +https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages. + +Use this index to find package docs, changelogs, and release notes for Remix v3 +alpha.6. + +## Critical alpha.6 changes + +- `remix/component` package exports were removed; use `remix/ui`, + `remix/ui/jsx-runtime`, `remix/ui/jsx-dev-runtime`, and `remix/ui/server`. +- `MultipartPart.headers` is now a decoded plain object with lower-case keys; + use `part.headers['content-type']` instead of + `part.headers.get('content-type')`. +- The Remix CLI is now exposed through `remix/cli` and the `remix` binary, with + Node.js 24.3.0 or newer declared in package metadata. + +## Release notes + +- [alpha.6 release notes for all package versions](./release-notes.md) ## 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) +- [remix](./remix/index.md) - The Remix web framework + ([changelog](./remix/changelog.md)) +- [cli](./cli/index.md) - Command-line interface for Remix + ([changelog](./cli/changelog.md)) +- [ui](./ui/index.md) - UI tokens, mixins, and glyphs for Remix components + ([changelog](./ui/changelog.md); 17 nested docs) +- [fetch-router](./fetch-router/index.md) - A minimal, composable router for the + web Fetch API ([changelog](./fetch-router/changelog.md)) +- [route-pattern](./route-pattern/index.md) - Match and generate URLs with + strong typing ([changelog](./route-pattern/changelog.md)) +- [node-fetch-server](./node-fetch-server/index.md) - Build servers for Node.js + using the web fetch API ([changelog](./node-fetch-server/changelog.md)) +- [test](./test/index.md) - A test framework for JavaScript and TypeScript + projects ([changelog](./test/changelog.md)) + +## UI and assets + +- [ui](./ui/index.md) - UI tokens, mixins, and glyphs for Remix components + ([changelog](./ui/changelog.md); 17 nested docs) +- [assets](./assets/index.md) - Fetch-based server for compiling browser JS/TS + and CSS assets on demand ([changelog](./assets/changelog.md)) +- [html-template](./html-template/index.md) - HTML template tag with + auto-escaping for JavaScript ([changelog](./html-template/changelog.md)) + +## Routing, requests, and middleware + +- [fetch-router](./fetch-router/index.md) - A minimal, composable router for the + web Fetch API ([changelog](./fetch-router/changelog.md)) +- [route-pattern](./route-pattern/index.md) - Match and generate URLs with + strong typing ([changelog](./route-pattern/changelog.md)) +- [node-fetch-server](./node-fetch-server/index.md) - Build servers for Node.js + using the web fetch API ([changelog](./node-fetch-server/changelog.md)) +- [fetch-proxy](./fetch-proxy/index.md) - An HTTP proxy for the web Fetch API + ([changelog](./fetch-proxy/changelog.md)) +- [async-context-middleware](./async-context-middleware/index.md) - Middleware + for storing request context in AsyncLocalStorage + ([changelog](./async-context-middleware/changelog.md)) +- [auth-middleware](./auth-middleware/index.md) - Pluggable authentication + middleware for Remix ([changelog](./auth-middleware/changelog.md)) +- [compression-middleware](./compression-middleware/index.md) - Middleware for + compressing HTTP responses + ([changelog](./compression-middleware/changelog.md)) +- [cop-middleware](./cop-middleware/index.md) - Middleware for tokenless + cross-origin protection in Fetch API servers + ([changelog](./cop-middleware/changelog.md)) +- [cors-middleware](./cors-middleware/index.md) - Middleware for handling CORS + in Fetch API servers ([changelog](./cors-middleware/changelog.md)) +- [csrf-middleware](./csrf-middleware/index.md) - Middleware for CSRF protection + in Fetch API servers ([changelog](./csrf-middleware/changelog.md)) +- [logger-middleware](./logger-middleware/index.md) - Middleware for logging + HTTP requests and responses ([changelog](./logger-middleware/changelog.md)) +- [method-override-middleware](./method-override-middleware/index.md) - + Middleware for overriding HTTP request methods from form data + ([changelog](./method-override-middleware/changelog.md)) +- [static-middleware](./static-middleware/index.md) - Middleware for serving + static files from the filesystem + ([changelog](./static-middleware/changelog.md)) + +## Auth, sessions, and cookies + +- [auth](./auth/index.md) - Browser login, OAuth, and OIDC helpers for Remix + ([changelog](./auth/changelog.md)) +- [auth-middleware](./auth-middleware/index.md) - Pluggable authentication + middleware for Remix ([changelog](./auth-middleware/changelog.md)) +- [session](./session/index.md) - Session management for JavaScript + ([changelog](./session/changelog.md)) +- [session-middleware](./session-middleware/index.md) - Middleware for managing + sessions with cookie-based storage + ([changelog](./session-middleware/changelog.md)) +- [session-storage-memcache](./session-storage-memcache/index.md) - Memcache + session storage for remix/session + ([changelog](./session-storage-memcache/changelog.md)) +- [session-storage-redis](./session-storage-redis/index.md) - Redis session + storage for remix/session ([changelog](./session-storage-redis/changelog.md)) +- [cookie](./cookie/index.md) - A toolkit for working with cookies in JavaScript + ([changelog](./cookie/changelog.md)) +- [csrf-middleware](./csrf-middleware/index.md) - Middleware for CSRF protection + in Fetch API servers ([changelog](./csrf-middleware/changelog.md)) + +## Data and storage + +- [data-schema](./data-schema/index.md) - Tiny, standards-aligned schema + validation ([changelog](./data-schema/changelog.md)) +- [data-table](./data-table/index.md) - A typed, relational query toolkit for + Remix ([changelog](./data-table/changelog.md)) +- [data-table-mysql](./data-table-mysql/index.md) - MySQL adapter for + remix/data-table ([changelog](./data-table-mysql/changelog.md)) +- [data-table-postgres](./data-table-postgres/index.md) - PostgreSQL adapter for + remix/data-table ([changelog](./data-table-postgres/changelog.md)) +- [data-table-sqlite](./data-table-sqlite/index.md) - SQLite adapter for + remix/data-table ([changelog](./data-table-sqlite/changelog.md)) +- [file-storage](./file-storage/index.md) - Key/value storage for JavaScript + File objects ([changelog](./file-storage/changelog.md)) +- [file-storage-s3](./file-storage-s3/index.md) - S3 backend for + remix/file-storage ([changelog](./file-storage-s3/changelog.md)) +- [fs](./fs/index.md) - Filesystem utilities using the Web File API + ([changelog](./fs/changelog.md)) +- [lazy-file](./lazy-file/index.md) - Lazy, streaming files for JavaScript + ([changelog](./lazy-file/changelog.md)) + +## Responses, headers, uploads, and parsing + +- [response](./response/index.md) - Response helpers for the web Fetch API + ([changelog](./response/changelog.md)) +- [headers](./headers/index.md) - A toolkit for working with HTTP headers in + JavaScript ([changelog](./headers/changelog.md)) +- [form-data-middleware](./form-data-middleware/index.md) - Middleware for + parsing FormData from request bodies + ([changelog](./form-data-middleware/changelog.md)) +- [form-data-parser](./form-data-parser/index.md) - A request.formData() wrapper + with streaming file upload handling + ([changelog](./form-data-parser/changelog.md)) +- [multipart-parser](./multipart-parser/index.md) - A fast, efficient parser for + multipart streams in any JavaScript environment + ([changelog](./multipart-parser/changelog.md)) +- [mime](./mime/index.md) - Utilities for working with MIME types + ([changelog](./mime/changelog.md)) +- [tar-parser](./tar-parser/index.md) - A fast, efficient parser for tar streams + in any JavaScript environment ([changelog](./tar-parser/changelog.md)) + +## Testing and terminal utilities + +- [test](./test/index.md) - A test framework for JavaScript and TypeScript + projects ([changelog](./test/changelog.md)) +- [terminal](./terminal/index.md) - Terminal output utilities for JavaScript + libraries and CLIs ([changelog](./terminal/changelog.md)) +- [assert](./assert/index.md) - Node assert-compatible utilities for any + JavaScript environment ([changelog](./assert/changelog.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) | +| Package | Version | Focus | Docs | +| -------------------------- | ------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| assert | 0.1.0 | Node assert-compatible utilities for any JavaScript environment | [assert](./assert/index.md) + [changelog](./assert/changelog.md) | +| assets | 0.2.0 | Fetch-based server for compiling browser JS/TS and CSS assets on demand | [assets](./assets/index.md) + [changelog](./assets/changelog.md) | +| async-context-middleware | 0.2.1 | Middleware for storing request context in AsyncLocalStorage | [async-context-middleware](./async-context-middleware/index.md) + [changelog](./async-context-middleware/changelog.md) | +| auth | 0.2.0 | Browser login, OAuth, and OIDC helpers for Remix | [auth](./auth/index.md) + [changelog](./auth/changelog.md) | +| auth-middleware | 0.1.1 | Pluggable authentication middleware for Remix | [auth-middleware](./auth-middleware/index.md) + [changelog](./auth-middleware/changelog.md) | +| cli | 0.1.0 | Command-line interface for Remix | [cli](./cli/index.md) + [changelog](./cli/changelog.md) | +| compression-middleware | 0.1.6 | Middleware for compressing HTTP responses | [compression-middleware](./compression-middleware/index.md) + [changelog](./compression-middleware/changelog.md) | +| cookie | 0.5.1 | A toolkit for working with cookies in JavaScript | [cookie](./cookie/index.md) + [changelog](./cookie/changelog.md) | +| cop-middleware | 0.1.1 | Middleware for tokenless cross-origin protection in Fetch API servers | [cop-middleware](./cop-middleware/index.md) + [changelog](./cop-middleware/changelog.md) | +| cors-middleware | 0.1.1 | Middleware for handling CORS in Fetch API servers | [cors-middleware](./cors-middleware/index.md) + [changelog](./cors-middleware/changelog.md) | +| csrf-middleware | 0.1.1 | Middleware for CSRF protection in Fetch API servers | [csrf-middleware](./csrf-middleware/index.md) + [changelog](./csrf-middleware/changelog.md) | +| data-schema | 0.3.0 | Tiny, standards-aligned schema validation | [data-schema](./data-schema/index.md) + [changelog](./data-schema/changelog.md) | +| data-table | 0.2.0 | A typed, relational query toolkit for Remix | [data-table](./data-table/index.md) + [changelog](./data-table/changelog.md) | +| data-table-mysql | 0.3.0 | MySQL adapter for remix/data-table | [data-table-mysql](./data-table-mysql/index.md) + [changelog](./data-table-mysql/changelog.md) | +| data-table-postgres | 0.3.0 | PostgreSQL adapter for remix/data-table | [data-table-postgres](./data-table-postgres/index.md) + [changelog](./data-table-postgres/changelog.md) | +| data-table-sqlite | 0.4.0 | SQLite adapter for remix/data-table | [data-table-sqlite](./data-table-sqlite/index.md) + [changelog](./data-table-sqlite/changelog.md) | +| fetch-proxy | 0.8.0 | An HTTP proxy for the web Fetch API | [fetch-proxy](./fetch-proxy/index.md) + [changelog](./fetch-proxy/changelog.md) | +| fetch-router | 0.18.1 | A minimal, composable router for the web Fetch API | [fetch-router](./fetch-router/index.md) + [changelog](./fetch-router/changelog.md) | +| file-storage | 0.13.4 | Key/value storage for JavaScript File objects | [file-storage](./file-storage/index.md) + [changelog](./file-storage/changelog.md) | +| file-storage-s3 | 0.1.1 | S3 backend for remix/file-storage | [file-storage-s3](./file-storage-s3/index.md) + [changelog](./file-storage-s3/changelog.md) | +| form-data-middleware | 0.2.2 | Middleware for parsing FormData from request bodies | [form-data-middleware](./form-data-middleware/index.md) + [changelog](./form-data-middleware/changelog.md) | +| form-data-parser | 0.17.0 | A request.formData() wrapper with streaming file upload handling | [form-data-parser](./form-data-parser/index.md) + [changelog](./form-data-parser/changelog.md) | +| fs | 0.4.3 | Filesystem utilities using the Web File API | [fs](./fs/index.md) + [changelog](./fs/changelog.md) | +| headers | 0.19.0 | A toolkit for working with HTTP headers in JavaScript | [headers](./headers/index.md) + [changelog](./headers/changelog.md) | +| html-template | 0.3.0 | HTML template tag with auto-escaping for JavaScript | [html-template](./html-template/index.md) + [changelog](./html-template/changelog.md) | +| lazy-file | 5.0.3 | Lazy, streaming files for JavaScript | [lazy-file](./lazy-file/index.md) + [changelog](./lazy-file/changelog.md) | +| logger-middleware | 0.2.0 | Middleware for logging HTTP requests and responses | [logger-middleware](./logger-middleware/index.md) + [changelog](./logger-middleware/changelog.md) | +| method-override-middleware | 0.1.6 | Middleware for overriding HTTP request methods from form data | [method-override-middleware](./method-override-middleware/index.md) + [changelog](./method-override-middleware/changelog.md) | +| mime | 0.4.1 | Utilities for working with MIME types | [mime](./mime/index.md) + [changelog](./mime/changelog.md) | +| multipart-parser | 0.16.0 | A fast, efficient parser for multipart streams in any JavaScript environment | [multipart-parser](./multipart-parser/index.md) + [changelog](./multipart-parser/changelog.md) | +| node-fetch-server | 0.13.0 | Build servers for Node.js using the web fetch API | [node-fetch-server](./node-fetch-server/index.md) + [changelog](./node-fetch-server/changelog.md) | +| remix | 3.0.0-alpha.6 | The Remix web framework | [remix](./remix/index.md) + [changelog](./remix/changelog.md) | +| response | 0.3.3 | Response helpers for the web Fetch API | [response](./response/index.md) + [changelog](./response/changelog.md) | +| route-pattern | 0.20.1 | Match and generate URLs with strong typing | [route-pattern](./route-pattern/index.md) + [changelog](./route-pattern/changelog.md) | +| session | 0.4.1 | Session management for JavaScript | [session](./session/index.md) + [changelog](./session/changelog.md) | +| session-middleware | 0.2.1 | Middleware for managing sessions with cookie-based storage | [session-middleware](./session-middleware/index.md) + [changelog](./session-middleware/changelog.md) | +| session-storage-memcache | 0.1.0 | Memcache session storage for remix/session | [session-storage-memcache](./session-storage-memcache/index.md) + [changelog](./session-storage-memcache/changelog.md) | +| session-storage-redis | 0.1.0 | Redis session storage for remix/session | [session-storage-redis](./session-storage-redis/index.md) + [changelog](./session-storage-redis/changelog.md) | +| static-middleware | 0.4.7 | Middleware for serving static files from the filesystem | [static-middleware](./static-middleware/index.md) + [changelog](./static-middleware/changelog.md) | +| tar-parser | 0.7.1 | A fast, efficient parser for tar streams in any JavaScript environment | [tar-parser](./tar-parser/index.md) + [changelog](./tar-parser/changelog.md) | +| terminal | 0.1.0 | Terminal output utilities for JavaScript libraries and CLIs | [terminal](./terminal/index.md) + [changelog](./terminal/changelog.md) | +| test | 0.2.0 | A test framework for JavaScript and TypeScript projects | [test](./test/index.md) + [changelog](./test/changelog.md) | +| ui | 0.1.0 | UI tokens, mixins, and glyphs for Remix components | [ui](./ui/index.md) + [changelog](./ui/changelog.md) + [17 docs](./ui/docs/) | ## Update instructions -See [update](./update.md) for how to sync this documentation from upstream. +1. Use + `gh release view remix@<version> --repo remix-run/remix --json name,tagName,body,publishedAt` + to fetch the umbrella release notes. +2. Fetch each package release noted by the umbrella release with + `gh release view <package>@<version> --repo remix-run/remix --json name,tagName,body,publishedAt`. +3. Delete `docs/agents/remix` and redownload package `README.md`, + `CHANGELOG.md`, and `docs/**/*.md` from the matching Remix tag. +4. Regenerate this index and `release-notes.md`, then run + `bun run format:check`. 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<eventMap>` 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<DrummerEventMap> { - 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<DrummerEvent, Drummer> - }, -}) -``` - -## 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/changelog.md b/docs/agents/remix/lazy-file/changelog.md new file mode 100644 index 0000000..51c45e8 --- /dev/null +++ b/docs/agents/remix/lazy-file/changelog.md @@ -0,0 +1,193 @@ +# `lazy-file` CHANGELOG + +This is the changelog for +[`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file). +It follows [semantic versioning](https://semver.org/). + +## v5.0.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + +## v5.0.2 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0) + +## v5.0.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v5.0.0 + +### Major Changes + +- `LazyFile` and `LazyBlob` no longer extend native `File` and `Blob` + + Some runtimes (like Bun) bypass the JavaScript layer when accessing + `File`/`Blob` internals, leading to issues with missing content due to the + lazy loading behavior. `LazyFile` and `LazyBlob` now implement the same + interface as their native counterparts but are standalone classes. + + As a result: + - `lazyFile instanceof File` now returns `false` + - You cannot pass `LazyFile`/`LazyBlob` directly to `new Response(file)` or + `formData.append('file', file)` + - Passing a `LazyFile`/`LazyBlob` directly to `Response` will throw an error + with guidance on correct usage + + **Migration:** + + ```ts + // Before + let response = new Response(lazyFile) + + // After - streaming + let response = new Response(lazyFile.stream()) + + // After - for non-streaming APIs that require a complete File (e.g. FormData) + formData.append('file', await lazyFile.toFile()) + ``` + + **New methods added:** + - `LazyFile.toFile()` + - `LazyFile.toBlob()` + - `LazyBlob.toBlob()` + + **Note:** `.toFile()` and `.toBlob()` read the entire content into memory. + Only use these for non-streaming APIs that require a complete `File` or `Blob` + (e.g. `FormData`). Always prefer `.stream()` when possible. + +## v4.2.0 (2025-11-26) + +- Move `@remix-run/mime` to `peerDependencies` + +## v4.1.0 (2025-11-25) + +- Replaced `mrmime` dependency with `@remix-run/mime` for MIME type detection + +## v4.0.0 (2025-11-20) + +- BREAKING CHANGE: Removed `lazy-file/fs` export. Use `@remix-run/fs` package + instead. + + ```ts + // before + import { openFile, writeFile } from '@remix-run/lazy-file/fs' + + // after + import { openFile, writeFile } from '@remix-run/fs' + ``` + +## v3.8.0 (2025-11-18) + +- BREAKING CHANGE: `openFile()` now sets `file.name` to the `filename` argument + as provided, instead of using `path.basename(filename)`. You can still + override this with `options.name`. + +```ts +// before +let file = openFile('./public/assets/favicon.ico') +file.name // "favicon.ico" + +// after +let file = openFile('./public/assets/favicon.ico') +file.name // "./public/assets/favicon.ico" + +// You can still override the name +let file = openFile('./public/assets/favicon.ico', { name: 'favicon.ico' }) +file.name // "favicon.ico" +``` + +## v3.7.0 (2025-11-04) + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. +- Fix type errors in TypeScript 5.7+ when using typed arrays + +## v3.6.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v3.5.0 (2025-07-21) + +- Renamed package from `@mjackson/lazy-file` to `@remix-run/lazy-file` + +## v3.4.0 (2025-06-10) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v3.3.1 (2025-01-25) + +- Handle stream errors in `lazy-file/fs`' `writeFile`. When there is an error in + the stream, call `writeStream.end()` on the underlying file stream before + rejecting the promise. + +## v3.3.0 (2024-11-14) + +- Add CommonJS build + +## v3.2.0 (2024-09-12) + +- Export `OpenFileOptions` from `lazy-file/fs` + +## v3.1.0 (2024-09-04) + +- Add writeFile method to `lazy-file/fs` and rename `getFile` => `openFile` +- Accept an open file descriptor or file handle in `writeFile(fd)` + +## v3.0.0 (2024-08-25) + +- BREAKING: Do not accept regular string argument to `LazyFile`. This more + closely matches `File` behavior +- BREAKING: Move 4th `LazyFile()` argument `range` into `options.range` +- BREAKING: Renamed `LazyFileContent` interface to `LazyContent` and + `content.read()` => `content.stream()` +- Added `LazyBlob` (`Blob` subclass) as a complement to `LazyFile` +- Added `LazyBlobOptions` and `LazyFileOptions` interfaces (`endings` is not + supported) +- Return a `name`-less `Blob` from `file.slice()` to more closely match native + `File` behavior + +## v2.2.0 (2024-08-24) + +- Added support for `getFile(, { lastModified })` to override + `file.lastModified` +- Export `GetFileOptions` interface from `lazy-file/fs` + +## v2.1.0 (2024-08-24) + +- Added `getFile` helper to `lazy-file/fs` export for reading files from the + local filesystem + +## v2.0.0 (2024-08-23) + +- BREAKING: Do not automatically propagate `name` and `lastModified` in + `file.slice()`. This matches the behavior of `File` more closely +- BREAKING: Remove `LazyFile[Symbol.asyncIterator]` to match the behavior of + `File` more closely +- In `slice(start, end)` make `end` default to `size` instead of `Infinity`. + This more closely matches the `File` spec +- Small perf improvement when streaming content arrays with Blobs in them and + ending early + +## v1.1.0 (2024-08-22) + +- Add ability to initialize a LazyFile with `BlobPart[]`, just like a normal + `File` +- Add async iterator support to LazyFile + +## v1.0.0 (2024-08-21) + +- Initial release diff --git a/docs/agents/remix/lazy-file.md b/docs/agents/remix/lazy-file/index.md similarity index 90% rename from docs/agents/remix/lazy-file.md rename to docs/agents/remix/lazy-file/index.md index d4162c3..dcb370d 100644 --- a/docs/agents/remix/lazy-file.md +++ b/docs/agents/remix/lazy-file/index.md @@ -1,10 +1,8 @@ -# lazy-file - -Source: https://github.com/remix-run/remix/tree/main/packages/lazy-file +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/lazy-file --> -## README +# lazy-file -`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 +27,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 +54,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 +64,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 +97,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 +136,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/changelog.md b/docs/agents/remix/logger-middleware/changelog.md new file mode 100644 index 0000000..e3da0b9 --- /dev/null +++ b/docs/agents/remix/logger-middleware/changelog.md @@ -0,0 +1,61 @@ +# `logger-middleware` CHANGELOG + +This is the changelog for +[`logger-middleware`](https://github.com/remix-run/remix/tree/main/packages/logger-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.2.0 + +### Minor Changes + +- Colorize high-signal logger tokens when terminal color detection allows it by + default, with a `colors` option to force colorized output on or off and + support for `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, and TTY output + streams when the `process` global is defined. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + +## v0.1.5 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.4 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.1.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + +## v0.1.2 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + +## v0.1.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.1.0 (2025-11-19) + +Initial release extracted from `@remix-run/fetch-router` v0.9.0. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/logger-middleware/README.md) +for more details. diff --git a/docs/agents/remix/logger-middleware.md b/docs/agents/remix/logger-middleware/index.md similarity index 61% rename from docs/agents/remix/logger-middleware.md rename to docs/agents/remix/logger-middleware/index.md index 0455555..673ab53 100644 --- a/docs/agents/remix/logger-middleware.md +++ b/docs/agents/remix/logger-middleware/index.md @@ -1,25 +1,30 @@ -# logger-middleware +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/logger-middleware --> -Source: https://github.com/remix-run/remix/tree/main/packages/logger-middleware +# logger-middleware -## README +HTTP request/response logging middleware for Remix. It logs request metadata and +response details with configurable output formats. -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). +## Features -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 +- **Colorized Output** - Highlights method, status, duration, and content length + in TTY output ## 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 +38,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 @@ -76,6 +81,31 @@ let router = createRouter({ }) ``` +### Colorized Output + +Logger output automatically uses ANSI colors for high-signal tokens when +terminal color detection allows them. Set `colors` to `false` to disable +colorized output or `true` to force it on. When the `process` global is defined, +color detection respects `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, and TTY +output streams. + +```ts +let router = createRouter({ + middleware: [ + logger({ + colors: false, + }), + ], +}) +``` + +The following tokens are colorized when colors are enabled: + +- `%method` +- `%status` +- `%duration` +- `%contentLength` + ### Custom Logger You can use a custom logger to write logs to a file or other stream. @@ -104,7 +134,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/changelog.md b/docs/agents/remix/method-override-middleware/changelog.md new file mode 100644 index 0000000..e4fdea6 --- /dev/null +++ b/docs/agents/remix/method-override-middleware/changelog.md @@ -0,0 +1,51 @@ +# `method-override-middleware` CHANGELOG + +This is the changelog for +[`method-override-middleware`](https://github.com/remix-run/remix/tree/main/packages/method-override-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.1.6 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.1.5 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.1.4 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + +## v0.1.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + +## v0.1.2 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.1.1 (2025-11-25) + +- Re-use request methods from `fetch-router` + +## v0.1.0 (2025-11-19) + +Initial release extracted from `@remix-run/fetch-router` v0.9.0. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/method-override-middleware/README.md) +for more details. diff --git a/docs/agents/remix/method-override-middleware.md b/docs/agents/remix/method-override-middleware/index.md similarity index 70% rename from docs/agents/remix/method-override-middleware.md rename to docs/agents/remix/method-override-middleware/index.md index 86fed5f..a14b59d 100644 --- a/docs/agents/remix/method-override-middleware.md +++ b/docs/agents/remix/method-override-middleware/index.md @@ -1,20 +1,21 @@ -# method-override-middleware +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/method-override-middleware --> -Source: -https://github.com/remix-run/remix/tree/main/packages/method-override-middleware +# method-override-middleware -## README +Method override middleware for Remix. It allows HTML forms to simulate `PUT`, +`PATCH`, and `DELETE` requests using a hidden form field. -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). +## Features -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 +27,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 +80,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/changelog.md b/docs/agents/remix/mime/changelog.md new file mode 100644 index 0000000..3167932 --- /dev/null +++ b/docs/agents/remix/mime/changelog.md @@ -0,0 +1,53 @@ +# `mime` CHANGELOG + +This is the changelog for +[`mime`](https://github.com/remix-run/remix/tree/main/packages/mime). It follows +[semantic versioning](https://semver.org/). + +## v0.4.1 + +### Patch Changes + +- Prefer `video/mp4` for `.mp4` files and `image/x-icon` for `.ico` files. + +## v0.4.0 + +### Minor Changes + +- Include all MIME types from mime-db, including experimental (`x-`) and + vendor-specific (`vnd.`) types. + +## v0.3.0 + +### Minor Changes + +- Add `defineMimeType()` for registering custom MIME types. This allows adding + support for file extensions not included in the defaults, or overriding + existing behavior. Custom registrations take precedence over built-in types. + + ```ts + import { defineMimeType, detectMimeType } from '@remix-run/mime' + + defineMimeType({ + extensions: 'myformat', + mimeType: 'application/x-myformat', + }) + + detectMimeType('file.myformat') // 'application/x-myformat' + ``` + +## v0.2.0 (2025-12-18) + +- Add `detectContentType(extension)` function that returns a Content-Type header + value with `charset` for text-based types. + +- Add `mimeTypeToContentType(mimeType)` function that converts a MIME type to a + Content-Type header value, adding `charset` for text-based types. + +## v0.1.0 (2025-11-25) + +Initial release of this package. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/mime/README.md) +for more details. diff --git a/docs/agents/remix/mime.md b/docs/agents/remix/mime/index.md similarity index 73% rename from docs/agents/remix/mime.md rename to docs/agents/remix/mime/index.md index 81ce6fd..0ea271b 100644 --- a/docs/agents/remix/mime.md +++ b/docs/agents/remix/mime/index.md @@ -1,18 +1,22 @@ -# @remix-run/mime +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/mime --> -Source: https://github.com/remix-run/remix/tree/main/packages/mime +# mime -## README +MIME type detection and content-type helpers for Remix. This package maps +extensions to MIME types and provides utilities for charset and compressibility +checks. -Utilities for working with MIME types. +## Features -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 +26,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 +42,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 +55,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 +66,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 +82,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 +95,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 +118,3 @@ defineMimeType({ ## License MIT - -## Navigation - -- [Remix package index](./index.md) 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/changelog.md b/docs/agents/remix/multipart-parser/changelog.md new file mode 100644 index 0000000..a0f4d8d --- /dev/null +++ b/docs/agents/remix/multipart-parser/changelog.md @@ -0,0 +1,266 @@ +# `multipart-parser` CHANGELOG + +This is the changelog for +[`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser). +It follows [semantic versioning](https://semver.org/). + +## v0.16.0 + +### Minor Changes + +- BREAKING CHANGE: `MultipartPart.headers` is now a plain decoded object keyed + by lower-case header name instead of a native `Headers` instance. Access part + headers with bracket notation like `part.headers['content-type']` instead of + `part.headers.get('content-type')`. + + This lets multipart part headers preserve decoded UTF-8 field names and + filenames that native `Headers` cannot store. + +## v0.15.0 + +### Minor Changes + +- BREAKING CHANGE: `parseMultipart()`, `parseMultipartStream()`, and + `parseMultipartRequest()` now enforce finite default `maxParts` and + `maxTotalSize` limits, and add `MaxPartsExceededError` and + `MaxTotalSizeExceededError` for handling multipart envelope limit failures. + + Apps that intentionally accept large multipart requests may need to raise + `maxParts` or `maxTotalSize` explicitly. + +## v0.14.2 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.14.1 + +### Patch Changes + +- Update `@remix-run/headers` peer dependency to use the new header parsing + methods. + +## v0.14.0 (2025-11-26) + +- Move `@remix-run/headers` to `peerDependencies` + +## v0.13.0 (2025-11-04) + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.12.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v0.11.0 (2025-07-24) + +- Renamed package from `@mjackson/multipart-parser` to + `@remix-run/multipart-parser` + +## v0.10.1 (2025-06-13) + +- Add doc comments on custom error classes + +## v0.10.0 (2025-06-13) + +This release represents a major refactoring and simplification of this library +from a `async`/promise-based architecture to a generator that suspends the +parser as parts are found. + +This is a reversion to the generator-based interface used before `v0.8` when I +switched to a promise interface to get around deadlock issues with consuming +part streams inside a `yield` suspension point. The deadlock occurred when +trying to read `part.body` inside a `yield`, because the parser was suspended +and wouldn't emit any more bytes to the stream while the consumer was waiting +for the stream to complete. + +With this release, I realized that instead of getting rid of the generator, +which is actually a fantastic interface for a streaming parser, I should've +gotten rid of the `part.body` stream instead and replaced it with a +`part.content` property that contains all the content for that part. This gives +us a better parser interface and also makes error handling simpler when e.g. the +parser's `maxFileSize` is exceeded. This also makes the parser easier to use +because you don't have to e.g. `await part.text()` anymore, and you have access +to `part.size` up front. + +- BREAKING CHANGE: `parseMultipart` and `parseMultipartRequest` are now + generators that `yield` `MultipartPart` objects as they are parsed +- BREAKING CHANGE: `parseMultipart` no longer parses streams, use + `parseMultipartStream` instead +- BREAKING CHANGE: `parser.parse()` is removed +- BREAKING CHANGE: `part.body`, `part.bodyUsed` are removed +- BREAKING CHANGE: `part.arrayBuffer`, `part.bytes`, `part.text` are now sync + getters instead of `async` methods +- BREAKING CHANGE: Default `maxFileSize` is now 2MiB, same as PHP's default + [`upload_max_filesize`](https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize) + +New APIs: + +- `parseMultipartStream(stream, options)` is a generator that parses a stream of + data +- `parser.write(chunk)` and `parser.finish()` are low-level APIs for running the + parser directly +- `part.content` is a `Uint8Array[]` of all content in that part +- `part.isText` is `true` if the part originates from a text field +- `part.size` is the total size of the content in bytes + +If you're upgrading, check the README for current usage recommendations. Here's +a high-level taste of the before/after of this release. + +```ts +import { parseMultipartRequest } from '@remix-run/multipart-parser' + +// before +await parseMultipartRequest(request, async (part) => { + let buffer = await part.arrayBuffer() + // ... +}) + +// after +for await (let part of parseMultipartRequest(request)) { + let buffer = part.arrayBuffer + // ... +} +``` + +## v0.9.0 (2025-06-10) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v0.8.2 (2025-02-04) + +- Add `Promise<void>` to `MultipartPartHandler` return type + +## v0.8.1 (2025-01-27) + +- Fix bad publish that left a `workspace:^` version identifier in package.json + +## v0.8.0 (2025-01-24) + +This release improves error handling and simplifies some of the internals of the +parser. + +- BREAKING CHANGE: Change `parseMultipartRequest` and `parseMultipart` + interfaces from `for await...of` to `await` + callback API. + +```ts +import { parseMultipartRequest } from '@remix-run/multipart-parser' + +// before +for await (let part of parseMultipartRequest(request)) { + // ... +} + +// after +await parseMultipartRequest(request, (part) => { + // ... +}) +``` + +This change greatly simplifies the implementation of +`parseMultipartRequest`/`parseMultipart` and fixes a subtle bug that did not +properly catch parse errors when `maxFileSize` was exceeded (see #28). + +- Add `MaxHeaderSizeExceededError` and `MaxFileSizeExceededError` to make it + easier to have finer-grained error handling. + +```ts +import * as http from 'node:http' +import { + MultipartParseError, + MaxFileSizeExceededError, + parseMultipartRequest, +} from '@remix-run/multipart-parser/node' + +const tenMb = 10 * Math.pow(2, 20) + +const server = http.createServer(async (req, res) => { + try { + await parseMultipartRequest(req, { maxFileSize: tenMb }, (part) => { + // ... + }) + } catch (error) { + if (error instanceof MaxFileSizeExceededError) { + res.writeHead(413) + res.end(error.message) + } else if (error instanceof MultipartParseError) { + res.writeHead(400) + res.end('Invalid multipart request') + } else { + console.error(error) + res.writeHead(500) + res.end('Internal Server Error') + } + } +}) +``` + +## v0.7.3 (2025-01-24) + +- Add support for environments that do not support + `ReadableStream.prototype[Symbol.asyncIterator]` (i.e. Safari), see #46 + +## v0.7.2 (2024-12-12) + +- Fix dependency on `headers` in package.json + +## v0.7.1 (2024-12-07) + +- Re-export everything from `multipart-parser/node`. If you're using + `multipart-parser/node`, you should `import` everything from there. Don't + import anything from `multipart-parser`. + +- ## v0.7.0 (2024-11-14) + +- Added CommonJS build + +## v0.6.3 (2024-09-05) + +- Moved to a new monorepo + +## v0.6.2 (2024-08-19) + +- Provide correct type for `part.arrayBuffer()` +- `part.isFile` now correctly detects + `part.mediaType === 'application/octet-stream'` + +## v0.6.1 (2024-08-18) + +- More small performance improvements + +## v0.6.0 (2024-08-17) + +- BREAKING: Removed some low-level API (`parser.push()` and `parser.reset()`) + that was duplicating higher-level API. Use `parser.parse()` instead. +- Added `parser.maxHeaderSize` and `parser.maxFileSize` properties +- Small performance improvements when parsing large files + +## v0.5.0 (2024-08-15) + +- Change default `maxFileSize` from 10 MB to `Infinity` +- Simplify internal buffer management and search, which leads to more consistent + chunk flow when handling large file uploads + +## v0.4.2 (2024-08-13) + +- Fix bug where max file size exceeded error would crash Node.js servers + (https://github.com/mjackson/multipart-parser/issues/8) + +## v0.4.1 (2024-08-12) + +- Add `type` keyword to `MultipartParserOptions` export for Deno + (https://github.com/mjackson/multipart-parser/pull/11) + +## v0.4.0 (2024-08-12) + +- Switch dependency from `fetch-super-headers` to `@remix-run/headers` +- Use `for await...of` to iterate over `ReadableStream` internally. This will + also cancel the stream when the loop exits from e.g. an error in a + user-defined `part` handler. diff --git a/docs/agents/remix/multipart-parser/index.md b/docs/agents/remix/multipart-parser/index.md index b71de1a..b78c343 100644 --- a/docs/agents/remix/multipart-parser/index.md +++ b/docs/agents/remix/multipart-parser/index.md @@ -1,45 +1,47 @@ -# multipart-parser - -Source: https://github.com/remix-run/remix/tree/main/packages/multipart-parser +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/multipart-parser --> -## Overview +# multipart-parser -`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. +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. -## Why multipart-parser? +## Features -- **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 +- **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 -bun add @remix-run/multipart-parser +npm i remix ``` ## 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. +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-run/multipart-parser' +} from 'remix/multipart-parser' async function handleRequest(request: Request): void { try { @@ -52,6 +54,7 @@ async function handleRequest(request: Request): void { ) console.log(`Content type: ${part.mediaType}`) console.log(`Field name: ${part.name}`) + console.log(`Content-Type header: ${part.headers['content-type']}`) // Save to disk, upload to cloud storage, etc. await saveFile(part.filename, part.bytes) @@ -70,9 +73,226 @@ async function handleRequest(request: Request): void { } ``` -## Navigation +## Part Headers + +Each `MultipartPart` exposes decoded part headers as a plain object keyed by +lower-case header name. Values are strings, and repeated headers are joined with +`, `. Multipart part headers are parsed metadata from the request body, not +native `Headers` objects, so access them with bracket notation: + +```ts +for await (let part of parseMultipartRequest(request)) { + let contentDisposition = part.headers['content-disposition'] + let contentType = part.headers['content-type'] + + console.log(contentDisposition, contentType) +} +``` + +## 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<Response> { + 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 `Content-Disposition` and `Content-Type` metadata for + each `MultipartPart` + +## Credits + +Thanks to Jacob Ebey who gave me several code reviews on this project prior to +publishing. + +## License -- [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) +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) 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<Response> { - 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/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/changelog.md b/docs/agents/remix/node-fetch-server/changelog.md new file mode 100644 index 0000000..535dfd4 --- /dev/null +++ b/docs/agents/remix/node-fetch-server/changelog.md @@ -0,0 +1,118 @@ +# `node-fetch-server` CHANGELOG + +This is the changelog for +[`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server). +It follows [semantic versioning](https://semver.org/). + +## v0.13.0 (2025-12-18) + +- Use the `:authority` header to set the URL of http/2 requests. + +## v0.12.0 (2025-11-04) + +- Use `tsc` directly instead of `esbuild` to build the package. This means + modules in the `dist` directory now mirror the layout of modules in the `src` + directory. + +## v0.11.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v0.10.0 (2025-10-04) + +- Fire `close` and `finish` listeners only once (#10757) + +## v0.9.0 (2025-09-16) + +- Support `statusText` in HTTP/1 responses (#10745) + +## v0.8.1 (2025-09-11) + +- Only abort `request.signal` when the connection closes before the response + completes (see #10726) + +## v0.8.0 (2025-07-24) + +- Renamed package from `@mjackson/node-fetch-server` to + `@remix-run/node-fetch-server` +- Handle backpressure correctly in response streaming + +## v0.7.0 (2025-06-06) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v0.6.1 (2025-02-07) + +- Update typings and docs for http2 support + +## v0.6.0 (2025-02-06) + +- Add http/2 support + +```ts +import * as http2 from 'node:http2' +import { createRequestListener } from '@remix-run/node-fetch-server' + +let server = http2.createSecureServer(options) + +server.on( + 'request', + createRequestListener((request) => { + let url = new URL(request.url) + + if (url.pathname === '/') { + return new Response('Hello HTTP/2!', { + headers: { + 'Content-Type': 'text/plain', + }, + }) + } + + return new Response('Not Found', { status: 404 }) + }), +) +``` + +## v0.5.1 (2025-01-25) + +- Iterate manually over response bodies in `sendResponse` instead of using + `for await...of`. This seems to avoid an issue where the iterator tries to + read from a stream after the lock has been released. + +## v0.5.0 (2024-12-09) + +- Expose `createHeaders(req: http.IncomingMessage): Headers` API for creating + headers from the headers of incoming request objects. +- Update `sendResponse` to use an object to add support for libraries such as + express while maintaining `node:http` and `node:https` compatibility. + +## v0.4.1 (2024-12-04) + +- Fix low-level API example in the README + +## v0.4.0 (2024-11-26) + +- BREAKING: Change `createRequest` signature to + `createRequest(req, res, options)` so the abort signal fires on the `res`'s + "end" event instead of `req` + +## v0.3.0 (2024-11-20) + +- Added `createRequest(req: http.IncomingMessage, options): Request` and + `sendResponse(res: http.ServerResponse, response: Response): Promise<void>` + exports to assist with building custom fetch servers + +## v0.2.0 (2024-11-14) + +- Small perf improvement from avoiding accessing `req.headers` and reading + `req.rawHeaders` instead +- Added CommonJS build + +## v0.1.0 (2024-09-05) + +- Initial release 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 index c0690ae..4faeefe 100644 --- a/docs/agents/remix/node-fetch-server/index.md +++ b/docs/agents/remix/node-fetch-server/index.md @@ -1,24 +1,18 @@ -# node-fetch-server - -Source: https://github.com/remix-run/remix/tree/main/packages/node-fetch-server - -## Overview +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/node-fetch-server --> -Build portable Node.js servers using web-standard Fetch API primitives. +# node-fetch-server -`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. +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` and `Response` APIs -- **Drop-in Integration** - Works with `node:http` and `node:https` +- **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) @@ -27,13 +21,324 @@ runtimes. ## Installation ```sh -bun add @remix-run/node-fetch-server +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 ``` -## Navigation +## 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 -- [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) +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) 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/release-notes.md b/docs/agents/remix/release-notes.md new file mode 100644 index 0000000..d56e5be --- /dev/null +++ b/docs/agents/remix/release-notes.md @@ -0,0 +1,980 @@ +# Remix v3 alpha.6 release notes + +Release notes retrieved with `gh release view ... --repo remix-run/remix` for +the umbrella Remix release and each package version present at +`remix@3.0.0-alpha.6`. + +## Contents + +- [remix@3.0.0-alpha.6](#remix300-alpha6) +- [assert@0.1.0](#assert010) +- [assets@0.2.0](#assets020) +- [async-context-middleware@0.2.1](#async-context-middleware021) +- [auth@0.2.0](#auth020) +- [auth-middleware@0.1.1](#auth-middleware011) +- [cli@0.1.0](#cli010) +- [compression-middleware@0.1.6](#compression-middleware016) +- [cookie@0.5.1](#cookie051) +- [cop-middleware@0.1.1](#cop-middleware011) +- [cors-middleware@0.1.1](#cors-middleware011) +- [csrf-middleware@0.1.1](#csrf-middleware011) +- [data-schema@0.3.0](#data-schema030) +- [data-table@0.2.0](#data-table020) +- [data-table-mysql@0.3.0](#data-table-mysql030) +- [data-table-postgres@0.3.0](#data-table-postgres030) +- [data-table-sqlite@0.4.0](#data-table-sqlite040) +- [fetch-proxy@0.8.0](#fetch-proxy080) +- [fetch-router@0.18.1](#fetch-router0181) +- [file-storage@0.13.4](#file-storage0134) +- [file-storage-s3@0.1.1](#file-storage-s3011) +- [form-data-middleware@0.2.2](#form-data-middleware022) +- [form-data-parser@0.17.0](#form-data-parser0170) +- [fs@0.4.3](#fs043) +- [headers@0.19.0](#headers0190) +- [html-template@0.3.0](#html-template030) +- [lazy-file@5.0.3](#lazy-file503) +- [logger-middleware@0.2.0](#logger-middleware020) +- [method-override-middleware@0.1.6](#method-override-middleware016) +- [mime@0.4.1](#mime041) +- [multipart-parser@0.16.0](#multipart-parser0160) +- [node-fetch-server@0.13.0](#node-fetch-server0130) +- [response@0.3.3](#response033) +- [route-pattern@0.20.1](#route-pattern0201) +- [session@0.4.1](#session041) +- [session-middleware@0.2.1](#session-middleware021) +- [session-storage-memcache@0.1.0](#session-storage-memcache010) +- [session-storage-redis@0.1.0](#session-storage-redis010) +- [static-middleware@0.4.7](#static-middleware047) +- [tar-parser@0.7.1](#tar-parser071) +- [terminal@0.1.0](#terminal010) +- [test@0.2.0](#test020) +- [ui@0.1.0](#ui010) + +## remix@3.0.0-alpha.6 + +- Name: remix v3.0.0-alpha.6 +- Published: 2026-04-29T17:32:47Z + +### Pre-release Changes + +- BREAKING CHANGE: `MultipartPart.headers` from `remix/multipart-parser` and + `remix/multipart-parser/node` is now a plain decoded object keyed by + lower-case header name instead of a native `Headers` instance. Access part + headers with bracket notation like `part.headers['content-type']` instead of + `part.headers.get('content-type')`. + +- BREAKING CHANGE: Removed the deprecated `remix/component`, + `remix/component/jsx-runtime`, `remix/component/jsx-dev-runtime`, and + `remix/component/server` package exports. Import the consolidated UI runtime + from `remix/ui`, `remix/ui/jsx-runtime`, `remix/ui/jsx-dev-runtime`, and + `remix/ui/server` instead. + + Removed `package.json` `bin` commands: + - `remix-test` + + Added `package.json` `exports`: + - `remix/node-fetch-server/test` to re-export APIs from + `@remix-run/node-fetch-server/test` + - `remix/terminal` to re-export APIs from `@remix-run/terminal` + - `remix/test/cli` to re-export APIs from `@remix-run/test/cli` + + Added `package.json` `exports` for the consolidated UI runtime: + - `remix/ui` to re-export APIs from `@remix-run/ui` + - `remix/ui/jsx-runtime` to re-export APIs from `@remix-run/ui/jsx-runtime` + - `remix/ui/jsx-dev-runtime` to re-export APIs from + `@remix-run/ui/jsx-dev-runtime` + - `remix/ui/server` to re-export APIs from `@remix-run/ui/server` + - `remix/ui/animation` to re-export APIs from `@remix-run/ui/animation` + - `remix/ui/accordion` to re-export APIs from `@remix-run/ui/accordion` + - `remix/ui/anchor` to re-export APIs from `@remix-run/ui/anchor` + - `remix/ui/breadcrumbs` to re-export APIs from `@remix-run/ui/breadcrumbs` + - `remix/ui/button` to re-export APIs from `@remix-run/ui/button` + - `remix/ui/combobox` to re-export APIs from `@remix-run/ui/combobox` + - `remix/ui/glyph` to re-export APIs from `@remix-run/ui/glyph` + - `remix/ui/listbox` to re-export APIs from `@remix-run/ui/listbox` + - `remix/ui/menu` to re-export APIs from `@remix-run/ui/menu` + - `remix/ui/popover` to re-export APIs from `@remix-run/ui/popover` + - `remix/ui/scroll-lock` to re-export APIs from `@remix-run/ui/scroll-lock` + - `remix/ui/select` to re-export APIs from `@remix-run/ui/select` + - `remix/ui/separator` to re-export APIs from `@remix-run/ui/separator` + - `remix/ui/theme` to re-export APIs from `@remix-run/ui/theme` + - `remix/ui/test` to re-export APIs from `@remix-run/ui/test` + +- Added `package.json` exports and binaries for the Remix CLI: + - `remix/cli` to expose the Remix CLI programmatic API + - `remix` as a `package.json` `bin` command that delegates to `@remix-run/cli` + + The Remix CLI now reads the current Remix version from the `remix` package and + declares Node.js 24.3.0 or later in package metadata. + +- Bumped `@remix-run/*` dependencies: + - [`assets@0.2.0`](https://github.com/remix-run/remix/releases/tag/assets@0.2.0) + - [`auth@0.2.0`](https://github.com/remix-run/remix/releases/tag/auth@0.2.0) + - [`cli@0.1.0`](https://github.com/remix-run/remix/releases/tag/cli@0.1.0) + - [`compression-middleware@0.1.6`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.6) + - [`data-schema@0.3.0`](https://github.com/remix-run/remix/releases/tag/data-schema@0.3.0) + - [`data-table-sqlite@0.4.0`](https://github.com/remix-run/remix/releases/tag/data-table-sqlite@0.4.0) + - [`fetch-proxy@0.8.0`](https://github.com/remix-run/remix/releases/tag/fetch-proxy@0.8.0) + - [`file-storage@0.13.4`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.4) + - [`file-storage-s3@0.1.1`](https://github.com/remix-run/remix/releases/tag/file-storage-s3@0.1.1) + - [`form-data-middleware@0.2.2`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.2.2) + - [`form-data-parser@0.17.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.17.0) + - [`fs@0.4.3`](https://github.com/remix-run/remix/releases/tag/fs@0.4.3) + - [`lazy-file@5.0.3`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.3) + - [`logger-middleware@0.2.0`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.2.0) + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + - [`multipart-parser@0.16.0`](https://github.com/remix-run/remix/releases/tag/multipart-parser@0.16.0) + - [`response@0.3.3`](https://github.com/remix-run/remix/releases/tag/response@0.3.3) + - [`static-middleware@0.4.7`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.7) + - [`tar-parser@0.7.1`](https://github.com/remix-run/remix/releases/tag/tar-parser@0.7.1) + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + - [`test@0.2.0`](https://github.com/remix-run/remix/releases/tag/test@0.2.0) + - [`ui@0.1.0`](https://github.com/remix-run/remix/releases/tag/ui@0.1.0) + +## assert@0.1.0 + +- Name: assert v0.1.0 +- Published: 2026-04-23T00:44:16Z + +### Minor Changes + +- Initial release of `@remix-run/assert`. + + A compatible subset of `node:assert/strict` that works in any JavaScript + environment, including browsers. Uses strict equality (`===`) for all + comparisons — no type coercion. + - `AssertionError` — compatible with `node:assert.AssertionError` (`actual`, + `expected`, `operator`, `name`) + - `assert.ok` — truthy check + - `assert.equal` / `assert.notEqual` — strict equality (`===` / `!==`) + - `assert.deepEqual` / `assert.notDeepEqual` — recursive strict deep equality + - `assert.match` — string matches a regexp + - `assert.fail` — unconditional failure + - `assert.throws` — synchronous throw assertion + - `assert.rejects` — async rejection assertion + +## assets@0.2.0 + +- Name: assets v0.2.0 +- Published: 2026-04-29T17:32:36Z + +### Minor Changes + +- BREAKING CHANGE: `target` configuration is now configured at the top level + with an object format, supporting `es` version targets along with browser + version targets. + + Browser targets are configured with string versions such as + `target: { chrome: '109', safari: '16.4' }`, and scripts can specify `es` as a + year of `2015` or higher such as `target: { es: '2020' }`. + + To migrate existing script configuration, replace `scripts.target` options + like `scripts: { target: 'es2020' }` with `target: { es: '2020' }`. + +- BREAKING CHANGE: Shared compiler options are now provided at the top level of + `createAssetServer()`. Use `sourceMaps`, `sourceMapSourcePaths`, and `minify` + directly on the asset server options instead of being nested under `scripts`. + This allows these options to also be used for styles as well as scripts. + + To migrate existing configuration, move `scripts.minify`, + `scripts.sourceMaps`, `scripts.sourceMapSourcePaths` to the top-level asset + server options. + +- `createAssetServer()` now compiles and serves `.css` files alongside scripts, + including local `@import` rewriting, fingerprinting, and shared compiler + options for minification, source maps, and browser compatibility targeting. + +### Patch Changes + +- Fix matching of dot-prefixed files and directories in `allow` and `deny` globs + +- Improve asset server import errors to include the resolved file path when a + resolved import is later rejected by validation for allow/deny rules, + supported file types and `fileMap` configuration. + +## async-context-middleware@0.2.1 + +- Name: async-context-middleware v0.2.1 +- Published: 2026-04-23T00:39:58Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## auth@0.2.0 + +- Name: auth v0.2.0 +- Published: 2026-04-29T17:32:45Z + +### Minor Changes + +- Added `createAtmosphereAuthProvider(options)` to support atproto OAuth flows + against Atmosphere-compatible authorization servers. + + The new provider resolves handles and DIDs with + `provider.prepare(handleOrDid)` before redirecting, performs required pushed + authorization requests with DPoP, supports both public web clients and + localhost loopback development clients, and seals per-session DPoP state into + the in-flight OAuth transaction using the required `sessionSecret` option + instead of a separate persistent store. + + Create the Atmosphere provider once with shared options, call + `provider.prepare(handleOrDid)` only before `startExternalAuth()`, and pass + the module-scope provider directly to `finishExternalAuth()` and + `refreshExternalAuth()`. Atmosphere callback results preserve the DPoP binding + state and authorization server refresh details alongside the returned + `accessToken` and `refreshToken`, so callers can reuse the completed token + bundle directly for refresh-token exchange and follow-up DPoP-signed requests. + +- Added `refreshExternalAuth()` to `@remix-run/auth` so apps can exchange stored + refresh tokens for fresh OAuth and OIDC token bundles. + + The built-in OIDC providers, X, and Atmosphere now implement refresh-token + exchange. Refreshed token bundles preserve the existing refresh token when the + provider omits a rotated value. + +## auth-middleware@0.1.1 + +- Name: auth-middleware v0.1.1 +- Published: 2026-04-23T00:40:01Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## cli@0.1.0 + +- Name: cli v0.1.0 +- Published: 2026-04-29T17:32:45Z + +### Minor Changes + +- Initial release of `@remix-run/cli` with the public `runRemix()` API and + commands for project scaffolding, health checks and fixes, route inspection, + skills syncing, and running tests. The package requires Node.js 24.3.0 or + later and exposes the programmatic CLI API; use the `remix` package for the + user-facing `remix` executable. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`tar-parser@0.7.1`](https://github.com/remix-run/remix/releases/tag/tar-parser@0.7.1) + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + - [`test@0.2.0`](https://github.com/remix-run/remix/releases/tag/test@0.2.0) + +## compression-middleware@0.1.6 + +- Name: compression-middleware v0.1.6 +- Published: 2026-04-29T17:32:42Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + - [`response@0.3.3`](https://github.com/remix-run/remix/releases/tag/response@0.3.3) + +## cookie@0.5.1 + +- Name: cookie v0.5.1 +- Published: 2026-02-28T01:27:38Z + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## cop-middleware@0.1.1 + +- Name: cop-middleware v0.1.1 +- Published: 2026-04-23T00:39:58Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## cors-middleware@0.1.1 + +- Name: cors-middleware v0.1.1 +- Published: 2026-04-23T00:40:00Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## csrf-middleware@0.1.1 + +- Name: csrf-middleware v0.1.1 +- Published: 2026-04-23T00:40:03Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## data-schema@0.3.0 + +- Name: data-schema v0.3.0 +- Published: 2026-04-29T17:32:35Z + +### Minor Changes + +- Add `Schema.transform()` for mapping validated schema outputs to new values + and output types. + +## data-table@0.2.0 + +- Name: data-table v0.2.0 +- Published: 2026-03-25T23:06:15Z + +### Minor Changes + +- BREAKING CHANGE: Rename adapter operation contracts and fields. + + `AdapterStatement` becomes `DataManipulationOperation`, and `statement` + becomes `operation`. + + Add separate adapter execution methods for DML and migration/DDL operations: + `execute` for `DataManipulationOperation` requests and `migrate` for + `DataMigrationOperation` requests. + + Add adapter introspection methods with optional transaction context: + `hasTable(table, transaction?)` and `hasColumn(table, column, transaction?)`. + +- BREAKING CHANGE: Replace the public `QueryBuilder` API with `Query` objects + that can be created with `query(table)` and executed with `db.exec(...)`. + + `db.query(table)` still provides the same chainable ergonomics, but it now + returns the public `Query` class in a database-bound form instead of a + separate `QueryBuilder` type. `db.exec(...)` now accepts only raw SQL or + `Query` values, and unbound terminal methods like `first()`, `count()`, + `exists()`, `insert()`, `update()`, and `delete()` return `Query` objects + instead of separate command descriptor types. + + The incidental `QueryMethod` type export has also been removed; use + `Database['query']` or `QueryForTable<table>` when you need that type shape. + +- BREAKING CHANGE: Remove the `@remix-run/data-table/sql` export. Import + `SqlStatement`, `sql`, and `rawSql` from `@remix-run/data-table` instead. + + `@remix-run/data-table/sql-helpers` remains available as the adapter-facing + SQL helper module. + +- BREAKING CHANGE: Rename the top-level table-definition helper from + `createTable(...)` to `table(...)` and switch column definitions to + `column(...)` builders. Runtime validation is now optional and table-scoped + via `validate({ operation, tableName, value })`. + + Remove `~standard` table-schema compatibility and + `getTableValidationSchemas(...)`, and stop runtime validation/coercion for + predicate values. + +- `@remix-run/data-table` now exports `Database` as the runtime class instead of + separating the runtime implementation from a structural `Database` type. You + can construct databases directly with `new Database(adapter, options)` or keep + using `createDatabase(adapter, options)`, which now delegates to the class + constructor. + +- Add a first-class migration system under `remix/data-table/migrations` with: + - `createMigration(...)` and timestamp-based migration loading + - chainable `column` builders plus schema APIs for create, alter, drop, and + index work + - `createMigrationRunner(adapter, migrations)` for `up`, `down`, `status`, and + `dryRun` + - migration journaling, checksum tracking, and optional Node loading from + `remix/data-table/migrations/node` + + Migration callbacks now use split handles: `{ db, schema }`. + - `db` is the immediate data runtime + (`query/create/update/delete/exec/transaction`) + - `schema` owns migration operations like `createTable`, `alterTable`, `plan`, + and introspection + + Migration-time DDL, DML, and introspection now share the same transaction + token when migration transactions are enabled. In `dryRun`, schema + introspection (`schema.hasTable` / `schema.hasColumn`) reads live + adapter/database state and does not simulate pending dry-run operations. + + Add public subpath exports for migrations, Node migration loading, SQL + helpers, operators, and SQL builders. SQL compilation stays adapter-owned, + while shared SQL compiler helpers remain available from + `remix/data-table/sql-helpers`. + + `@remix-run/data-table/migrations` no longer exports a separate `Database` + type alias. Migration callbacks still receive `context.db` as the main + `Database` runtime, so if you need the type directly, import `Database` from + `@remix-run/data-table` instead. + +- Add optional table lifecycle callbacks for write/delete/read flows: + `beforeWrite`, `afterWrite`, `beforeDelete`, `afterDelete`, and `afterRead`. + + Add `fail(...)` as a helper for returning structured validation/lifecycle + issues from `validate(...)`, `beforeWrite(...)`, and `beforeDelete(...)`. + +## data-table-mysql@0.3.0 + +- Name: data-table-mysql v0.3.0 +- Published: 2026-04-23T00:40:05Z + +### Minor Changes + +- BREAKING CHANGE: Removed adapter options + + **Affected APIs** + - `MysqlDatabaseAdapterOptions` type: removed + - `createMysqlDatabaseAdapter` function: `options` arg removed + - `MysqlDatabaseAdapter` constructor: `options` arg removed + + **Why** + + Adapter options existed solely for tests to override adapter capabilities. If + you must override capabilities, you can do so directly via mutation: + + ```ts + let adapter = createMysqlDatabaseAdapter(mysql) + adapter.capabilities = { + ...adapter.capabilities, + upsert: false, + } + ``` + +## data-table-postgres@0.3.0 + +- Name: data-table-postgres v0.3.0 +- Published: 2026-04-23T00:40:06Z + +### Minor Changes + +- BREAKING CHANGE: Removed adapter options + + **Affected APIs** + - `PostgresDatabaseAdapterOptions` type: removed + - `createPostgresDatabaseAdapter` function: `options` arg removed + - `PostgresDatabaseAdapter` constructor: `options` arg removed + + **Why** + + Adapter options existed solely for tests to override adapter capabilities. If + you must override capabilities, you can do so directly via mutation: + + ```ts + let adapter = createPostgresDatabaseAdapter(postgres) + adapter.capabilities = { + ...adapter.capabilities, + returning: false, + } + ``` + +- Types for `createPostgresDatabaseAdapter` now accept a `Client` in addition to + `Pool` and `PoolClient`. + + This is a type-only change that aligns the function signature with existing + runtime behavior. + +## data-table-sqlite@0.4.0 + +- Name: data-table-sqlite v0.4.0 +- Published: 2026-04-29T17:32:37Z + +### Minor Changes + +- Widened `createSqliteDatabaseAdapter` to accept synchronous SQLite clients + that match the shared `prepare`/`exec` surface used by Node's `node:sqlite`, + Bun's `bun:sqlite`, and compatible clients. The package no longer requires + `better-sqlite3` as an optional peer dependency. + +## fetch-proxy@0.8.0 + +- Name: fetch-proxy v0.8.0 +- Published: 2026-04-29T17:32:38Z + +### Minor Changes + +- Add an `X-Forwarded-Port` header when `xForwardedHeaders` is enabled. + +## fetch-router@0.18.1 + +- Name: fetch-router v0.18.1 +- Published: 2026-04-23T00:39:58Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`route-pattern@0.20.1`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.20.1) + +## file-storage@0.13.4 + +- Name: file-storage v0.13.4 +- Published: 2026-04-29T17:32:43Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fs@0.4.3`](https://github.com/remix-run/remix/releases/tag/fs@0.4.3) + - [`lazy-file@5.0.3`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.3) + +## file-storage-s3@0.1.1 + +- Name: file-storage-s3 v0.1.1 +- Published: 2026-04-29T17:32:46Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`file-storage@0.13.4`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.4) + +## form-data-middleware@0.2.2 + +- Name: form-data-middleware v0.2.2 +- Published: 2026-04-29T17:32:43Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`form-data-parser@0.17.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.17.0) + +## form-data-parser@0.17.0 + +- Name: form-data-parser v0.17.0 +- Published: 2026-04-29T17:32:39Z + +### Minor Changes + +- BREAKING CHANGE: Errors thrown or rejected by a `parseFormData()` upload + handler now propagate directly instead of being wrapped in a + `FormDataParseError`. + +### Patch Changes + +- Preserve non-ASCII multipart field names and filenames when parsing + `multipart/form-data` requests. + +- Bumped `@remix-run/*` dependencies: + - [`multipart-parser@0.16.0`](https://github.com/remix-run/remix/releases/tag/multipart-parser@0.16.0) + +## fs@0.4.3 + +- Name: fs v0.4.3 +- Published: 2026-04-29T17:32:40Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`lazy-file@5.0.3`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.3) + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + +## headers@0.19.0 + +- Name: headers v0.19.0 +- Published: 2026-02-28T01:27:44Z + +### Minor Changes + +- BREAKING CHANGE: Removed `Headers`/`SuperHeaders` class and default export. + Use the native `Headers` class with the static `from()` method on each header + class instead. + + New individual header `.from()` methods: + - `Accept.from()` + - `AcceptEncoding.from()` + - `AcceptLanguage.from()` + - `CacheControl.from()` + - `ContentDisposition.from()` + - `ContentRange.from()` + - `ContentType.from()` + - `Cookie.from()` + - `IfMatch.from()` + - `IfNoneMatch.from()` + - `IfRange.from()` + - `Range.from()` + - `SetCookie.from()` + - `Vary.from()` + + New raw header utilities added: + - `parse()` + - `stringify()` + + Migration example: + + ```ts + // Before: + import SuperHeaders from '@remix-run/headers' + let headers = new SuperHeaders(request.headers) + let mediaType = headers.contentType.mediaType + + // After: + import { ContentType } from '@remix-run/headers' + let contentType = ContentType.from(request.headers.get('content-type')) + let mediaType = contentType.mediaType + ``` + + If you were using the `Headers` constructor to parse raw HTTP header strings, + use `parse()` instead: + + ```ts + // Before: + import SuperHeaders from '@remix-run/headers' + let headers = new SuperHeaders( + 'Content-Type: text/html\r\nCache-Control: no-cache', + ) + + // After: + import { parse } from '@remix-run/headers' + let headers = parse('Content-Type: text/html\r\nCache-Control: no-cache') + ``` + + If you were using `headers.toString()` to convert headers to raw format, use + `stringify()` instead: + + ```ts + // Before: + import SuperHeaders from '@remix-run/headers' + let headers = new SuperHeaders() + headers.set('Content-Type', 'text/html') + let rawHeaders = headers.toString() + + // After: + import { stringify } from '@remix-run/headers' + let headers = new Headers() + headers.set('Content-Type', 'text/html') + let rawHeaders = stringify(headers) + ``` + +## html-template@0.3.0 + +- Name: html-template v0.3.0 +- Published: 2025-11-05T00:08:34Z + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## lazy-file@5.0.3 + +- Name: lazy-file v5.0.3 +- Published: 2026-04-29T17:32:38Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + +## logger-middleware@0.2.0 + +- Name: logger-middleware v0.2.0 +- Published: 2026-04-29T17:32:41Z + +### Minor Changes + +- Colorize high-signal logger tokens when terminal color detection allows it by + default, with a `colors` option to force colorized output on or off and + support for `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, and TTY output + streams when the `process` global is defined. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + +## method-override-middleware@0.1.6 + +- Name: method-override-middleware v0.1.6 +- Published: 2026-04-23T00:40:04Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## mime@0.4.1 + +- Name: mime v0.4.1 +- Published: 2026-04-29T17:32:36Z + +### Patch Changes + +- Prefer `video/mp4` for `.mp4` files and `image/x-icon` for `.ico` files. + +## multipart-parser@0.16.0 + +- Name: multipart-parser v0.16.0 +- Published: 2026-04-29T17:32:39Z + +### Minor Changes + +- BREAKING CHANGE: `MultipartPart.headers` is now a plain decoded object keyed + by lower-case header name instead of a native `Headers` instance. Access part + headers with bracket notation like `part.headers['content-type']` instead of + `part.headers.get('content-type')`. + + This lets multipart part headers preserve decoded UTF-8 field names and + filenames that native `Headers` cannot store. + +## node-fetch-server@0.13.0 + +- Name: node-fetch-server v0.13.0 +- Published: 2025-12-18T22:04:38Z + +- Use the `:authority` header to set the URL of http/2 requests. + +## response@0.3.3 + +- Name: response v0.3.3 +- Published: 2026-04-29T17:32:41Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + +## route-pattern@0.20.1 + +- Name: route-pattern v0.20.1 +- Published: 2026-04-23T00:39:59Z + +### Patch Changes + +- Matches return decoded values for params in hostname + + ```ts + pattern = new RoutePattern('://:subdomain.example.com/posts/:slug') + + url = new URL('https://café.example.com/posts/123') + pattern.match(url)?.params.subdomain + // Before -> 'xn--caf-dma' + // After -> 'café' + + url = new URL('https://北京.example.com/posts/123') + pattern.match(url)?.params.subdomain + // Before -> 'xn--1lq90i' + // After -> '北京' + ``` + +## session@0.4.1 + +- Name: session v0.4.1 +- Published: 2025-12-06T20:36:30Z + +- Always delete the original session ID when it is regenerated with the + `deleteOldSession` option. Intermediate IDs are never saved to storage, so + they can't be deleted. + +## session-middleware@0.2.1 + +- Name: session-middleware v0.2.1 +- Published: 2026-04-23T00:40:01Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## session-storage-memcache@0.1.0 + +- Name: session-storage-memcache v0.1.0 +- Published: 2026-02-28T01:27:49Z + +### Minor Changes + +- Add Memcache session storage with + `createMemcacheSessionStorage(server, options)`. + + This adds a Node.js Memcache backend with support for `useUnknownIds`, + `keyPrefix`, and `ttlSeconds`, along with integration tests that run against + Memcached in CI. + +## session-storage-redis@0.1.0 + +- Name: session-storage-redis v0.1.0 +- Published: 2026-02-28T01:27:49Z + +### Minor Changes + +- Initial release of `@remix-run/session-storage-redis` with + `createRedisSessionStorage()`. + +## static-middleware@0.4.7 + +- Name: static-middleware v0.4.7 +- Published: 2026-04-29T17:32:47Z + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fs@0.4.3`](https://github.com/remix-run/remix/releases/tag/fs@0.4.3) + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + - [`response@0.3.3`](https://github.com/remix-run/remix/releases/tag/response@0.3.3) + +## tar-parser@0.7.1 + +- Name: tar-parser v0.7.1 +- Published: 2026-04-29T17:32:44Z + +### Patch Changes + +- Fix parsing tar entries whose file body ends exactly at a chunk boundary. + +## terminal@0.1.0 + +- Name: terminal v0.1.0 +- Published: 2026-04-29T17:32:48Z + +### Minor Changes + +- Initial release of terminal output utilities for ANSI styles, color capability + detection, escape sequences, and testable terminal streams. Automatic color + detection disables styles for CI, `NO_COLOR`, `TERM=dumb`, and non-TTY output + streams by default, and can be overridden with the `colors` option. Style + helpers include common modifiers, foreground colors, background colors, bright + variants, and preserve outer styles when nested formatted strings close inner + styles. + +## test@0.2.0 + +- Name: test v0.2.0 +- Published: 2026-04-29T17:32:34Z + +### Minor Changes + +- Add `glob.exclude` config for filtering paths during test discovery (defaults + to `node_modules/**`) + +- Add code coverage reporting to `remix-test` + - You can enable coverage with default settings vis `remix-test --coverage` or + setting `coverage:true` in your `remix-test.config.ts` + - Or you can specify individual coverage settings via the following config + fields: + - `coverage.dir`: Directory to store coverage information (default + `.coverage`) + - `coverage.include`: Array of globs for files to include in coverage + - `coverage.exclude`: Array of globs for files to exclude from coverage + - `coverage.statements`: Percentage threshold for statement coverage + - `coverage.lines`: Percentage threshold for line coverage + - `coverage.branches`: Percentage threshold for branch coverage + - `coverage.functions`: Percentage threshold for function coverage + +- Export `runRemixTest` from `@remix-run/test/cli` so other tools can run the + Remix test runner programmatically without exiting the host process. The + function returns an exit code so callers can decide how to terminate. The + `remix-test` executable now declares Node.js 24.3.0 or later in package + metadata. + +### Patch Changes + +- Internal refactor to test discovery to better support test execution in `bun`. + - Unlike Node, Bun's `fs.promises.glob` _follows_ symbolic links and does not + prune traversal via the `exclude` option, which can cause the test runner to + enter `node_modules` symlink cycles in pnpm workspaces + - Refactored the internal test discovery logic to detect and use Bun's native + `Glob` class when running under the Bun runtime. Bun's `Glob#scan` does not + follow symlinks by default, avoiding the cycle. + - The Node runtime continues to use `fs.promises.glob` + +- Use native dynamic `import()` in Bun to load `.ts` and `.tsx` files in the + test runner + +- Bumped `@remix-run/*` dependencies: + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + +## ui@0.1.0 + +- Name: ui v0.1.0 +- Published: 2026-04-29T17:38:14Z + +### Minor Changes + +- BREAKING CHANGE: Consolidated the deprecated `@remix-run/component` package + into `@remix-run/ui`. Import component runtime APIs from `@remix-run/ui`, + server rendering APIs from `@remix-run/ui/server`, JSX runtime APIs from + `@remix-run/ui/jsx-runtime` and `@remix-run/ui/jsx-dev-runtime`, and animation + APIs from `@remix-run/ui/animation`. + + Removed the deprecated `@remix-run/ui/on-outside-pointer-down` export. Use the + popover, menu, or other component-level outside interaction APIs instead. + +- BREAKING CHANGE: Components now receive props through a stable `handle.props` + object using `Handle<Props, Context>` instead of receiving a separate `setup` + argument and render callback props. Move initialization values that previously + used `<Component setup={...} />` onto regular props, and read all props from + `handle.props` in both the component function and render callback. + + Before: + + ```tsx + function Counter( + handle: Handle<CounterContext>, + setup: { initialCount: number }, + ) { + let count = setup.initialCount + + return (props: { label: string }) => ( + <button> + {props.label}: {count} + </button> + ) + } + + ;<Counter setup={{ initialCount: 10 }} label="Count" /> + ``` + + After: + + ```tsx + function Counter( + handle: Handle<{ initialCount: number; label: string }, CounterContext>, + ) { + let count = handle.props.initialCount + + return () => ( + <button> + {handle.props.label}: {count} + </button> + ) + } + + ;<Counter initialCount={10} label="Count" /> + ``` + + The `handle.props` object keeps the same identity for the component lifetime + while its values are updated before each render, so destructuring + `let { props, update } = handle` remains safe. The `setup` prop is no longer + special and is treated like any other prop. + + This also removes the old pattern where setup-scope helpers had to read from a + mutable variable that was reassigned inside the render callback: + + ```tsx + function Listbox(handle: Handle<ListboxContext>) { + let props: ListboxProps + + function select(value: string) { + props.onSelect(value) + } + + handle.context.set({ select }) + + return (nextProps: ListboxProps) => { + props = nextProps + return props.children + } + } + ``` + + Helpers can now read the current props directly from the stable handle: + + ```tsx + function Listbox(handle: Handle<ListboxProps, ListboxContext>) { + function select(value: string) { + handle.props.onSelect(value) + } + + handle.context.set({ select }) + + return () => handle.props.children + } + ``` + +- BREAKING CHANGE: Removed the deprecated `keysEvents`, `pressEvents`, and + `PressEvent` exports from `@remix-run/ui`. Use `on(...)` with native DOM + keyboard, pointer, and click events directly instead. 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/changelog.md b/docs/agents/remix/remix/changelog.md new file mode 100644 index 0000000..faa73a4 --- /dev/null +++ b/docs/agents/remix/remix/changelog.md @@ -0,0 +1,287 @@ +# `remix` CHANGELOG + +This is the changelog for +[`remix`](https://github.com/remix-run/remix/tree/main/packages/remix). It +follows [semantic versioning](https://semver.org/). + +## v3.0.0-alpha.6 + +### Pre-release Changes + +- BREAKING CHANGE: `MultipartPart.headers` from `remix/multipart-parser` and + `remix/multipart-parser/node` is now a plain decoded object keyed by + lower-case header name instead of a native `Headers` instance. Access part + headers with bracket notation like `part.headers['content-type']` instead of + `part.headers.get('content-type')`. + +- BREAKING CHANGE: Removed the deprecated `remix/component`, + `remix/component/jsx-runtime`, `remix/component/jsx-dev-runtime`, and + `remix/component/server` package exports. Import the consolidated UI runtime + from `remix/ui`, `remix/ui/jsx-runtime`, `remix/ui/jsx-dev-runtime`, and + `remix/ui/server` instead. + + Removed `package.json` `bin` commands: + - `remix-test` + + Added `package.json` `exports`: + - `remix/node-fetch-server/test` to re-export APIs from + `@remix-run/node-fetch-server/test` + - `remix/terminal` to re-export APIs from `@remix-run/terminal` + - `remix/test/cli` to re-export APIs from `@remix-run/test/cli` + + Added `package.json` `exports` for the consolidated UI runtime: + - `remix/ui` to re-export APIs from `@remix-run/ui` + - `remix/ui/jsx-runtime` to re-export APIs from `@remix-run/ui/jsx-runtime` + - `remix/ui/jsx-dev-runtime` to re-export APIs from + `@remix-run/ui/jsx-dev-runtime` + - `remix/ui/server` to re-export APIs from `@remix-run/ui/server` + - `remix/ui/animation` to re-export APIs from `@remix-run/ui/animation` + - `remix/ui/accordion` to re-export APIs from `@remix-run/ui/accordion` + - `remix/ui/anchor` to re-export APIs from `@remix-run/ui/anchor` + - `remix/ui/breadcrumbs` to re-export APIs from `@remix-run/ui/breadcrumbs` + - `remix/ui/button` to re-export APIs from `@remix-run/ui/button` + - `remix/ui/combobox` to re-export APIs from `@remix-run/ui/combobox` + - `remix/ui/glyph` to re-export APIs from `@remix-run/ui/glyph` + - `remix/ui/listbox` to re-export APIs from `@remix-run/ui/listbox` + - `remix/ui/menu` to re-export APIs from `@remix-run/ui/menu` + - `remix/ui/popover` to re-export APIs from `@remix-run/ui/popover` + - `remix/ui/scroll-lock` to re-export APIs from `@remix-run/ui/scroll-lock` + - `remix/ui/select` to re-export APIs from `@remix-run/ui/select` + - `remix/ui/separator` to re-export APIs from `@remix-run/ui/separator` + - `remix/ui/theme` to re-export APIs from `@remix-run/ui/theme` + - `remix/ui/test` to re-export APIs from `@remix-run/ui/test` + +- Added `package.json` exports and binaries for the Remix CLI: + - `remix/cli` to expose the Remix CLI programmatic API + - `remix` as a `package.json` `bin` command that delegates to `@remix-run/cli` + + The Remix CLI now reads the current Remix version from the `remix` package and + declares Node.js 24.3.0 or later in package metadata. + +- Bumped `@remix-run/*` dependencies: + - [`assets@0.2.0`](https://github.com/remix-run/remix/releases/tag/assets@0.2.0) + - [`auth@0.2.0`](https://github.com/remix-run/remix/releases/tag/auth@0.2.0) + - [`cli@0.1.0`](https://github.com/remix-run/remix/releases/tag/cli@0.1.0) + - [`compression-middleware@0.1.6`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.6) + - [`data-schema@0.3.0`](https://github.com/remix-run/remix/releases/tag/data-schema@0.3.0) + - [`data-table-sqlite@0.4.0`](https://github.com/remix-run/remix/releases/tag/data-table-sqlite@0.4.0) + - [`fetch-proxy@0.8.0`](https://github.com/remix-run/remix/releases/tag/fetch-proxy@0.8.0) + - [`file-storage@0.13.4`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.4) + - [`file-storage-s3@0.1.1`](https://github.com/remix-run/remix/releases/tag/file-storage-s3@0.1.1) + - [`form-data-middleware@0.2.2`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.2.2) + - [`form-data-parser@0.17.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.17.0) + - [`fs@0.4.3`](https://github.com/remix-run/remix/releases/tag/fs@0.4.3) + - [`lazy-file@5.0.3`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.3) + - [`logger-middleware@0.2.0`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.2.0) + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + - [`multipart-parser@0.16.0`](https://github.com/remix-run/remix/releases/tag/multipart-parser@0.16.0) + - [`response@0.3.3`](https://github.com/remix-run/remix/releases/tag/response@0.3.3) + - [`static-middleware@0.4.7`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.7) + - [`tar-parser@0.7.1`](https://github.com/remix-run/remix/releases/tag/tar-parser@0.7.1) + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + - [`test@0.2.0`](https://github.com/remix-run/remix/releases/tag/test@0.2.0) + - [`ui@0.1.0`](https://github.com/remix-run/remix/releases/tag/ui@0.1.0) + +## v3.0.0-alpha.5 + +### Pre-release Changes + +- Added `package.json` `exports`: + - `remix/assert` to re-export APIs from `@remix-run/assert` + - `remix/test` to re-export APIs from `@remix-run/test` + + Added `package.json` `bin` commands: + - `remix-test` delegating to `@remix-run/test` + +- Bumped `@remix-run/*` dependencies: + - [`assert@0.1.0`](https://github.com/remix-run/remix/releases/tag/assert@0.1.0) + - [`assets@0.1.0`](https://github.com/remix-run/remix/releases/tag/assets@0.1.0) + - [`async-context-middleware@0.2.1`](https://github.com/remix-run/remix/releases/tag/async-context-middleware@0.2.1) + - [`auth@0.1.1`](https://github.com/remix-run/remix/releases/tag/auth@0.1.1) + - [`auth-middleware@0.1.1`](https://github.com/remix-run/remix/releases/tag/auth-middleware@0.1.1) + - [`component@0.7.0`](https://github.com/remix-run/remix/releases/tag/component@0.7.0) + - [`compression-middleware@0.1.5`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.5) + - [`cop-middleware@0.1.1`](https://github.com/remix-run/remix/releases/tag/cop-middleware@0.1.1) + - [`cors-middleware@0.1.1`](https://github.com/remix-run/remix/releases/tag/cors-middleware@0.1.1) + - [`csrf-middleware@0.1.1`](https://github.com/remix-run/remix/releases/tag/csrf-middleware@0.1.1) + - [`data-table-mysql@0.3.0`](https://github.com/remix-run/remix/releases/tag/data-table-mysql@0.3.0) + - [`data-table-postgres@0.3.0`](https://github.com/remix-run/remix/releases/tag/data-table-postgres@0.3.0) + - [`data-table-sqlite@0.3.0`](https://github.com/remix-run/remix/releases/tag/data-table-sqlite@0.3.0) + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + - [`form-data-middleware@0.2.1`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.2.1) + - [`logger-middleware@0.1.5`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.1.5) + - [`method-override-middleware@0.1.6`](https://github.com/remix-run/remix/releases/tag/method-override-middleware@0.1.6) + - [`route-pattern@0.20.1`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.20.1) + - [`session-middleware@0.2.1`](https://github.com/remix-run/remix/releases/tag/session-middleware@0.2.1) + - [`static-middleware@0.4.6`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.6) + - [`test@0.1.0`](https://github.com/remix-run/remix/releases/tag/test@0.1.0) + +## v3.0.0-alpha.4 + +### Pre-release Changes + +- BREAKING CHANGE: Remove the `remix/data-table/sql` export. Import + `SqlStatement`, `sql`, and `rawSql` from `remix/data-table` instead. + + `remix/data-table/sql-helpers` remains available for adapter-facing SQL + utilities. + + `remix/data-table` now exports the `Database` class as a runtime value. You + can construct a database directly with `new Database(adapter, options)` or + keep using `createDatabase(adapter, options)`, which now delegates to the + class constructor. + + BREAKING CHANGE: `remix/data-table` no longer exports `QueryBuilder`. Import + `Query` and `query` from `remix/data-table`, then execute unbound queries with + `db.exec(...)`. `db.exec(...)` now accepts only raw SQL or `Query` values, and + unbound terminal methods like `first()`, `count()`, `insert()`, and `update()` + return `Query` objects instead of separate command descriptor types. + `db.query(table)` remains available as shorthand and now returns the same + bound `Query` class. + + `remix/data-table/migrations` no longer exports a separate `Database` type + alias. Import `Database` from `remix/data-table` when you need the migration + `db` type directly. + + The incidental `QueryMethod` type export has also been removed; use + `Database['query']` or `QueryForTable<table>` when you need that type shape. + + Added `package.json` `exports`: + - `remix/auth-middleware` to re-export APIs from `@remix-run/auth-middleware` + - `remix/auth` to re-export APIs from `@remix-run/auth` + +- Add `remix/cors-middleware` to re-export the CORS middleware APIs from + `@remix-run/cors-middleware`. + +- Update `remix/ui` and `remix/ui/server` to re-export the latest + `@remix-run/ui` frame-navigation APIs. + + `remix/ui` now exposes `navigate(href, { src, target, history })`, + `link(href, { src, target, history })`, `run({ loadModule, resolveFrame })`, + and the `handle.frames.top` and `handle.frames.get(name)` helpers, while + `remix/ui/server` re-exports the SSR frame source APIs including `frameSrc`, + `topFrameSrc`, and `ResolveFrameContext`. + +- Add browser-origin and CSRF protection middleware APIs to `remix`. + - `remix/cop-middleware` exposes `cop(options)` for browser-focused + cross-origin protection using `Sec-Fetch-Site` with `Origin` fallback, + trusted origins, and configurable bypasses. + - `remix/csrf-middleware` exposes `csrf(options)` and `getCsrfToken(context)` + for session-backed CSRF tokens plus origin validation. + - Apps can use either middleware independently or layer `cop()`, `session()`, + and `csrf()` together when they want both browser-origin filtering and + token-backed protection. + +- Bumped `@remix-run/*` dependencies: + - [`async-context-middleware@0.2.0`](https://github.com/remix-run/remix/releases/tag/async-context-middleware@0.2.0) + - [`auth@0.1.0`](https://github.com/remix-run/remix/releases/tag/auth@0.1.0) + - [`auth-middleware@0.1.0`](https://github.com/remix-run/remix/releases/tag/auth-middleware@0.1.0) + - [`component@0.6.0`](https://github.com/remix-run/remix/releases/tag/component@0.6.0) + - [`compression-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.4) + - [`cop-middleware@0.1.0`](https://github.com/remix-run/remix/releases/tag/cop-middleware@0.1.0) + - [`cors-middleware@0.1.0`](https://github.com/remix-run/remix/releases/tag/cors-middleware@0.1.0) + - [`csrf-middleware@0.1.0`](https://github.com/remix-run/remix/releases/tag/csrf-middleware@0.1.0) + - [`data-schema@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-schema@0.2.0) + - [`data-table@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.2.0) + - [`data-table-mysql@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-table-mysql@0.2.0) + - [`data-table-postgres@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-table-postgres@0.2.0) + - [`data-table-sqlite@0.2.0`](https://github.com/remix-run/remix/releases/tag/data-table-sqlite@0.2.0) + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + - [`form-data-middleware@0.2.0`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.2.0) + - [`form-data-parser@0.16.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.16.0) + - [`logger-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.1.4) + - [`method-override-middleware@0.1.5`](https://github.com/remix-run/remix/releases/tag/method-override-middleware@0.1.5) + - [`multipart-parser@0.15.0`](https://github.com/remix-run/remix/releases/tag/multipart-parser@0.15.0) + - [`route-pattern@0.20.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.20.0) + - [`session-middleware@0.2.0`](https://github.com/remix-run/remix/releases/tag/session-middleware@0.2.0) + - [`static-middleware@0.4.5`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.5) + +## v3.0.0-alpha.3 + +### Pre-release Changes + +- Added `package.json` `exports`: + - `remix/data-schema` to re-export APIs from `@remix-run/data-schema` + - `remix/data-schema/checks` to re-export APIs from + `@remix-run/data-schema/checks` + - `remix/data-schema/coerce` to re-export APIs from + `@remix-run/data-schema/coerce` + - `remix/data-schema/lazy` to re-export APIs from + `@remix-run/data-schema/lazy` + - `remix/data-table` to re-export APIs from `@remix-run/data-table` + - `remix/data-table-mysql` to re-export APIs from + `@remix-run/data-table-mysql` + - `remix/data-table-postgres` to re-export APIs from + `@remix-run/data-table-postgres` + - `remix/data-table-sqlite` to re-export APIs from + `@remix-run/data-table-sqlite` + - `remix/fetch-router/routes` to re-export APIs from + `@remix-run/fetch-router/routes` + - `remix/file-storage-s3` to re-export APIs from `@remix-run/file-storage-s3` + - `remix/session-storage-memcache` to re-export APIs from + `@remix-run/session-storage-memcache` + - `remix/session-storage-redis` to re-export APIs from + `@remix-run/session-storage-redis` + +- Remove the root export from the `remix` package so you will no longer import + anything from `remix` and will instead always import from a sub-path such as + `remix/fetch-router` or `remix/ui` + +- Bumped `@remix-run/*` dependencies: + - [`async-context-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/async-context-middleware@0.1.3) + - [`component@0.5.0`](https://github.com/remix-run/remix/releases/tag/component@0.5.0) + - [`compression-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.3) + - [`data-schema@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-schema@0.1.0) + - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0) + - [`data-table-mysql@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table-mysql@0.1.0) + - [`data-table-postgres@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table-postgres@0.1.0) + - [`data-table-sqlite@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table-sqlite@0.1.0) + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + - [`file-storage@0.13.3`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.3) + - [`file-storage-s3@0.1.0`](https://github.com/remix-run/remix/releases/tag/file-storage-s3@0.1.0) + - [`form-data-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.1.4) + - [`fs@0.4.2`](https://github.com/remix-run/remix/releases/tag/fs@0.4.2) + - [`lazy-file@5.0.2`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.2) + - [`logger-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.1.3) + - [`method-override-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/method-override-middleware@0.1.4) + - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0) + - [`response@0.3.2`](https://github.com/remix-run/remix/releases/tag/response@0.3.2) + - [`route-pattern@0.19.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.19.0) + - [`session-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/session-middleware@0.1.4) + - [`session-storage-memcache@0.1.0`](https://github.com/remix-run/remix/releases/tag/session-storage-memcache@0.1.0) + - [`session-storage-redis@0.1.0`](https://github.com/remix-run/remix/releases/tag/session-storage-redis@0.1.0) + - [`static-middleware@0.4.4`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.4) + +## v3.0.0-alpha.2 + +### Pre-release Changes + +- Added `package.json` `exports`: + - `remix/route-pattern/specificity` to re-export APIs from + `@remix-run/route-pattern/specificity` + +- Bumped `@remix-run/*` dependencies: + - [`async-context-middleware@0.1.2`](https://github.com/remix-run/remix/releases/tag/async-context-middleware@0.1.2) + - [`component@0.4.0`](https://github.com/remix-run/remix/releases/tag/component@0.4.0) + - [`compression-middleware@0.1.2`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.2) + - [`fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + - [`form-data-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.1.3) + - [`form-data-parser@0.15.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.15.0) + - [`interaction@0.5.0`](https://github.com/remix-run/remix/releases/tag/interaction@0.5.0) + - [`logger-middleware@0.1.2`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.1.2) + - [`method-override-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/method-override-middleware@0.1.3) + - [`route-pattern@0.18.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.18.0) + - [`session-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/session-middleware@0.1.3) + - [`static-middleware@0.4.3`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.3) + +## v3.0.0-alpha.1 + +### Pre-release Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v3.0.0-alpha.0 + +### Major Changes + +- Initial alpha release of `remix` package for Remix 3 diff --git a/docs/agents/remix/remix/index.md b/docs/agents/remix/remix/index.md new file mode 100644 index 0000000..f542c60 --- /dev/null +++ b/docs/agents/remix/remix/index.md @@ -0,0 +1,59 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/remix --> + +# remix + +A modern web framework for JavaScript. + +See [remix.run](https://remix.run) for more information. + +## Installation + +```sh +npm i remix +``` + +## CLI + +Create a new app with the CLI: + +```sh +npx remix@next new my-remix-app +``` + +After installing `remix`, the equivalent local command and the rest of the CLI +are available through `remix`: + +```sh +remix new my-remix-app +remix completion bash >> ~/.bashrc +remix doctor +remix doctor --fix +remix routes +remix routes --table +remix routes --table --no-headers +remix skills install +remix test +remix version +remix --no-color doctor +``` + +## Programmatic CLI + +```ts +import { runRemix } from 'remix/cli' + +await runRemix(['new', 'my-remix-app']) +await runRemix(['completion', 'bash']) +await runRemix(['doctor']) +await runRemix(['doctor', '--fix']) +await runRemix(['routes']) +await runRemix(['routes', '--table']) +await runRemix(['routes', '--table', '--no-headers']) +await runRemix(['skills', 'list']) +await runRemix(['test']) +await runRemix(['version']) +``` + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/response/changelog.md b/docs/agents/remix/response/changelog.md new file mode 100644 index 0000000..301eb3f --- /dev/null +++ b/docs/agents/remix/response/changelog.md @@ -0,0 +1,100 @@ +# `response` CHANGELOG + +This is the changelog for +[`response`](https://github.com/remix-run/remix/tree/main/packages/response). It +follows [semantic versioning](https://semver.org/). + +## v0.3.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + +## v0.3.2 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0) + +## v0.3.1 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.3.0 + +### Minor Changes + +- `createFileResponse()` is now generic and accepts any file-like object + + The function now accepts any object satisfying the `FileLike` interface, which + includes both native `File` and `LazyFile` from `@remix-run/lazy-file`. This + change supports the updated `LazyFile` class which no longer extends native + `File`. + + The generic type flows through to the `digest` callback in options, so you get + the exact type you passed in: + + ```ts + // With native File - digest receives File + createFileResponse(nativeFile, request, { + digest: async (file) => { + /* file is typed as File */ + }, + }) + + // With LazyFile - digest receives LazyFile + createFileResponse(lazyFile, request, { + digest: async (file) => { + /* file is typed as LazyFile */ + }, + }) + ``` + +- Add `redirect` export which is a shorthand alias for `createRedirectResponse` + +### Patch Changes + +- Update `@remix-run/headers` peer dependency to use the new header parsing + methods. + +## v0.2.1 (2025-12-18) + +- `createFileResponse` now includes `charset` in Content-Type for text-based + files. + +## v0.2.0 (2025-11-25) + +- BREAKING CHANGE: Add `@remix-run/mime` as a peer dependency. This package is + used by the `createFileResponse()` response helper to determine if HTTP Range + requests should be supported by default for a given MIME type. + +- Add `compressResponse` helper + +- The `createFileResponse()` response helper now only enables HTTP Range + requests by default for non-compressible MIME types. This allows text-based + assets to be compressed while still supporting resumable downloads for media + files. + + To restore the previous behavior where all files support range requests: + + ```ts + return createFileResponse(file, request, { + acceptRanges: true, + }) + ``` + + Note: Range requests and compression are mutually exclusive. When + `Accept-Ranges: bytes` is present in response headers, the `compress()` + response helper and `compression()` middleware will not compress the response. + +## v0.1.0 (2025-11-25) + +Initial release with response helpers extracted from `@remix-run/fetch-router`. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/response/README.md) +for more details. 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('<h1>Hello, World!</h1>') -// Content-Type: text/html; charset=UTF-8 -// Body: <!DOCTYPE html><h1>Hello, World!</h1> -``` - -The helper automatically prepends `<!DOCTYPE html>` 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 = '<script>alert(1)</script>' -let response = createHtmlResponse(html`<h1>Hello, ${name}!</h1>`) -// 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 index eb5fedc..db8674e 100644 --- a/docs/agents/remix/response/index.md +++ b/docs/agents/remix/response/index.md @@ -1,29 +1,27 @@ -# response - -Source: https://github.com/remix-run/remix/tree/main/packages/response +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/response --> -## Overview +# response -Response helpers for the web Fetch API. `response` provides a collection of -helper functions for creating common HTTP responses with proper headers and -semantics. +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:** 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 +- [**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 -bun add @remix-run/response +npm i remix ``` ## Usage @@ -32,17 +30,247 @@ 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' +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('<h1>Hello, World!</h1>') +// Content-Type: text/html; charset=UTF-8 +// Body: <!DOCTYPE html><h1>Hello, World!</h1> +``` + +The helper automatically prepends `<!DOCTYPE html>` 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 = '<script>alert(1)</script>' +let response = createHtmlResponse(html`<h1>Hello, ${name}!</h1>`) +// 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, + }, + }, +}) ``` -## Navigation +#### 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 -- [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) +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) 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/changelog.md b/docs/agents/remix/route-pattern/changelog.md new file mode 100644 index 0000000..c31d48f --- /dev/null +++ b/docs/agents/remix/route-pattern/changelog.md @@ -0,0 +1,848 @@ +# `route-pattern` CHANGELOG + +This is the changelog for +[`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern). +It follows [semantic versioning](https://semver.org/). + +## v0.20.1 + +### Patch Changes + +- Matches return decoded values for params in hostname + + ```ts + pattern = new RoutePattern('://:subdomain.example.com/posts/:slug') + + url = new URL('https://café.example.com/posts/123') + pattern.match(url)?.params.subdomain + // Before -> 'xn--caf-dma' + // After -> 'café' + + url = new URL('https://北京.example.com/posts/123') + pattern.match(url)?.params.subdomain + // Before -> 'xn--1lq90i' + // After -> '北京' + ``` + +## v0.20.0 + +### Minor Changes + +- BREAKING CHANGE: Make search param pattern decoding and serialization + consistent with `URLSearchParams`. Affects: + `RoutePattern.{match,href,search,ast.search}` + + Previously, `RoutePattern` treated `?q` and `?q=` as **different** + constraints: + + ```ts + // Before: `?q` and `?q=` are different + + let url = new URL('https://example.com?q') + + // Matches "key only" constraint? + new RoutePattern('?q').match(url) // ✅ match + + // Matches "key and value" constraint? + new RoutePattern('?q=').match(url) // ❌ no match (`null`) + + // Different constraints serialized to different strings + new RoutePattern('?q').search // -> 'q' + new RoutePattern('?q=').search // -> 'q=' + ``` + + There were two main problems with that approach: + + **Unintuitive matching** + + ```ts + // URL search looks like `?q=` + let url = new URL('https://example.com?q=') + + // Pattern search looks like `?q=` + let pattern = new RoutePattern('?q=') + + // But "key and value" constraint doesn't match! + pattern.match(url) // ❌ no match (`null`) + ``` + + **Parsing and serialization** + + For consistency with `URLSearchParams`, search param patterns should be parsed + according to the + [WHATWG `application/x-www-form-urlencoded` parsing spec](https://url.spec.whatwg.org/#application/x-www-form-urlencoded-parsing) + and should also + [encode spaces as `+`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#percent_encoding). + + Now, we use `URLSearchParams` to parse search param patterns to guarantee + decodings are consistent: + + ```ts + let url = new URL('https://example.com?q=a+b') + // Decodes `+` to ` ` + url.searchParams.getAll('q') // -> ['a b'] + + // Before + let pattern = new RoutePattern('?q=a+b') + // Does not decode `+` to ` ` + pattern.ast.search.get('q') // -> ❌ Set { 'a+b' } + + // After + let pattern = new RoutePattern('?q=a+b') + // Decodes `+` to ` ` + pattern.ast.search.get('q') // -> ✅ Set { 'a b' } + ``` + + Similarly, now that `?q` and `?q=` are semantically equivalent, they should + serialize to the same thing: + + ```ts + new URLSearchParams('q=').toString() // 'q=' + + // Before + new RoutePattern('?q=').search // ❌ 'q' + + // After + new RoutePattern('?q=').search // ✅ 'q=' + ``` + + As a result, `RoutePattern`s can no longer represent a "key and any value" + constraint. In practice, this was a niche use-case so we chose correctness and + consistency with `URLSearchParams`. If the need for "key and any value" + constraints arises, we can later introduce a separate syntax for that without + the unintuitive shortcoming of `?q=`. + + With "key and any value" constraints removed, the `missing-search-param` error + type thrown by `RoutePattern.href` was made obsolete and was removed. + +- BREAKING CHANGE: `RoutePattern.ast` is now typed as deeply readonly. + + This was always the intended design; the type system now reflects it: + + ```ts + // Before + pattern.ast = { ...pattern.ast, protocol: 'https' } + pattern.ast.protocol = 'https' + pattern.ast.port = '443' + pattern.ast.hostname = null + pattern.ast.pathname = otherPattern.ast.pathname + pattern.ast.search.set('q', new Set(['x'])) + pattern.ast.pathname.tokens.push({ type: 'text', text: 'x' }) + pattern.ast.pathname.optionals.set(0, 1) + + // After + pattern.ast = { ...pattern.ast, protocol: 'https' } + // ~~~ + // Cannot assign to 'ast' because it is a read-only property. (2703) + + pattern.ast.protocol = 'https' + // ~~~~~~~~~ + // Cannot assign to 'protocol' because it is a read-only property. (2540) + + pattern.ast.port = '443' + // ~~~~ + // Cannot assign to 'port' because it is a read-only property. (2540) + + pattern.ast.hostname = null + // ~~~~~~~~ + // Cannot assign to 'hostname' because it is a read-only property. (2540) + + pattern.ast.pathname = otherPattern.ast.pathname + // ~~~~~~~~ + // Cannot assign to 'pathname' because it is a read-only property. (2540) + + pattern.ast.search.set('q', new Set(['x'])) + // ~~~ + // Property 'set' does not exist on type 'ReadonlyMap<string, ReadonlySet<string> | null>'. (2339) + + pattern.ast.pathname.tokens.push({ type: 'text', text: 'x' }) + // ~~~~ + // Property 'push' does not exist on type 'ReadonlyArray<PartPatternToken>'. (2339) + + pattern.ast.pathname.optionals.set(0, 1) + // ~~~ + // Property 'set' does not exist on type 'ReadonlyMap<number, number>'. (2339) + ``` + +- Matches return decoded values for params in pathname + + ```ts + let pattern = new RoutePattern('/posts/:slug') + + let url = new URL('https://blog.example.com/posts/💿') + pattern.match(url)?.params.slug + // Before -> '%F0%9F%92%BF' + // After -> '💿' + + url = new URL('https://blog.example.com/posts/café-hà-nội') + pattern.match(url)?.params.slug + // Before -> 'caf%C3%A9-h%C3%A0-n%E1%BB%99i' + // After -> 'café-hà-nội' + + url = new URL('https://blog.example.com/posts/北京') + pattern.match(url)?.params.slug + // Before -> '%E5%8C%97%E4%BA%AC' + // After -> '北京' + + url = new URL('https://blog.example.com/posts/مرحبا') + pattern.match(url)?.params.slug + // Before -> '%D9%85%D8%B1%D8%AD%D8%A8%D8%A7' + // After -> 'مرحبا' + ``` + + If you need percent-encoded text again, use + [`encodeURI`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI): + + ```ts + let url = new URL('https://blog.example.com/posts/💿') + let slug = pattern.match(url)!.params.slug + // -> 💿 + + encodeURI(slug) + // -> '%F0%9F%92%BF' + ``` + +### Patch Changes + +- Faster `TrieMatcher.match`: `O(m·log(m))` -> `O(m)` + + Previously, `TrieMatcher.match` internally called `.matchAll`, then sorted the + result to find the best match. For `m` matching route patterns, this took + `O(m·log(m))` operations. + + Now, `TrieMatcher.match` loops over the `m` matches, keeping track of the best + one, resulting in `O(n)` operations. + + In our benchmarks, this made our largest workload (~5000 route patterns) 17% + faster with negligible or modest improvements to other workloads. + +- Faster type inference for `RoutePattern.href`, `RoutePattern.match`, and + `Params` + + Reduced type instantiations for parsing param types, resulting in ~2-5x faster + in relevant + [type benchmarks](https://github.com/remix-run/remix/tree/main/packages/route-pattern/bench/types), + but varies depending on your route patterns. May fix + `"Type instantiation is excessively deep and possibly infinite" (ts2589)` for + some apps. + +## v0.19.0 + +### Minor Changes + +- BREAKING CHANGE: Remove `RoutePatternOptions` type and rework `ignoreCase` + + `RoutePattern.ignoreCase` field has been removed and `ignoreCase` now only + applies to `pathname` (no longer applies to `search`) + + Case sensitivity is now determined only when matching. + - `RoutePattern.match` now accept `ignoreCase` option + - `Matcher` constructors now accept `ignoreCase` option + + ```ts + // BEFORE + let pattern = new RoutePattern('/Posts/:id', { ignoreCase: true }) + pattern.match(url) + pattern.join(other, { ignoreCase: true }) + + let matcher = new ArrayMatcher() + + // AFTER + let pattern = new RoutePattern('/Posts/:id') + pattern.match(url) // default: ignoreCase = false + pattern.match(url, { ignoreCase: true }) + pattern.join(other) + + let arrayMatcher = new ArrayMatcher() // default: ignoreCase = false + // OR + let arrayMatcher = new ArrayMatcher({ ignoreCase: true }) + + let trieMatcher = new TrieMatcher() // default: ignoreCase = false + // OR + let trieMatcher = new TrieMatcher({ ignoreCase: true }) + ``` + +- BREAKING CHANGE: Change how params are represented within `RoutePattern.ast` + + Previously, `RoutePattern.ast.{hostname,pathname}.tokens` had param tokens + like: + + ```ts + type ParamToken = { type: ':'; '*'; nameIndex: number } + ``` + + where the `nameIndex` was used to access the param name from `paramNames`: + + ```ts + let { pathname } = pattern.ast + + for (let token of pathname.tokens) { + if (token.type === ':' || token.type === '*') { + let paramName = pathname.paramNames[token.nameIndex] + console.log('name: ', paramName) + } + } + ``` + + This has now been simplified so that param tokens contain their own name: + + ```ts + type ParamToken = { type: ':' | '*'; name: string } + + let { pathname } = pattern.ast + + for (let token of pathname.tokens) { + if (token.type === ':' || token.type === '*') { + console.log('name: ', token.name) + } + } + ``` + + If you want to iterate over _just_ the params, there's a new `.params` getter: + + ```ts + let { pathname } = pattern.ast + + for (let param of pathname.params) { + console.log('type: ', param.type) + console.log('name: ', param.name) + } + ``` + +- BREAKING CHANGE: Rename match `meta` to `paramsMeta` + + For `RoutePattern.match` and `type RoutePatternMatch`: + + ```ts + import { RoutePattern, type RoutePatternMatch } from 'remix/route-pattern' + + let pattern = new RoutePattern('...') + let match = pattern.match(url) + + // BEFORE + type Meta = RoutePatternMatch['meta'] + match.meta + + // AFTER + type ParamsMeta = RoutePatternMatch['paramsMeta'] + match.paramsMeta + ``` + + For `Matcher.match` and `type Match`: + + ```ts + import { Matcher, type Match } from 'remix/route-pattern' + + let matcher: Matcher = new ArrayMatcher() // Or TrieMatcher + + let match = matcher.match(url) + + // BEFORE + type Meta = Match['meta'] + match.meta + + // AFTER + type ParamsMeta = Match['paramsMeta'] + match.paramsMeta + ``` + +### Patch Changes + +- Previously, `href` was throwing an `HrefError` with `missing-params` type when + a nameless wildcard was encountered outside of an optional. But that was + misleading since nameless optionals aren't something the user should be + passing in values for. Instead, `href` now throws an `HrefError` with the + correct `nameless-wildcard` type for this case. + + Error messages have also been improved for many of the `HrefError` types. + Notably, the variants shown in `missing-params` were confusing since they + leaked internal formatting for params. That has been removed and the resulting + error message is now shorter and simpler. + +- Previously, including extra params in `RoutePattern.href` resulted in a type + error: + + ```ts + let pattern = new RoutePattern('/posts/:id') + pattern.href({ id: 1, extra: 'stuff' }) + // ^^^^^ + // 'extra' does not exist in type 'HrefParams<"/posts/:id">' + ``` + + Now, extra params are allowed and autocomplete for inferred params still + works: + + ```ts + let pattern = new RoutePattern('/posts/:id') + pattern.href({ id: 1, extra: 'stuff' }) // no type error + + pattern.href({}) + // ^ autocomplete suggests `id` + ``` + +- `ArrayMatcher.match` (optimized for small apps) got ~1.06x faster for our + small app benchmark. `TrieMatcher.match` (optimized for large apps) got ~1.17x + faster across the board. + +- Patterns with omitted port only match URLs with empty port `''` + + Previously, there was a bug that caused omitted ports in patterns to match any + ports. + +- `paramsMeta` shows a nameless wildcard match for omitted hostname + + An omitted hostname is already coerced to `*` (nameless wildcard) to represent + "match any hostname" during matching. Previously, `paramsMeta` did not + distinguish between a fully static hostname and an omitted hostname as both + had `hostname` set to `[]`. Now, `paramsMeta` returns a nameless wildcard + match for the entire hostname when the hostname is omitted. + + Example: + + ```ts + const pattern = new RoutePattern('/users/:id') + const match = pattern.match('http://example.com/users/123') + // match.paramsMeta.hostname is now [{ type: '*', name: '*', begin: 0, end: 11, value: 'example.com' }] + ``` + + As a result, `Specificity.descending` (the default ordering for matchers) now + correctly orders patterns with static hostname before patterns with omitted + hostnames. + +- `TrieMatcher` allows overlapping routes + + For example: + + ```ts + let pattern1 = new RoutePattern('://api.example.com/users/:id') + let pattern2 = new RoutePattern('://api.example.com/users(/:id)') + + matcher.add(pattern1) + matcher.add(pattern2) + ``` + + In this case, the second pattern fully overlaps the first one when the + optional is included and the TrieMatcher could not store overlapping routes, + so `pattern1` was silently dropped. + + Now, `TrieMatcher` allows overlapping routes by storing an array of route + patterns in the trie nodes. + +## v0.18.0 + +### Minor Changes + +- BREAKING CHANGE: Remove `createHrefBuilder`, `type HrefBuilder`, + `type HrefBuilderArg` + + `createHrefBuilder` was the original design and implementation of href + generation, but with the new `RoutePattern.href` method it is now obsolete. + + Use `HrefArgs` instead of `HrefBuilderArgs`: + + ```ts + // before + type Args = HrefBuilderArgs<Source> + + // after + type Args = HrefArgs<Source> + ``` + +- BREAKING CHANGE: simplify protocol to only accept `http`, `https`, and + `http(s)` + + Previously, we allowed arbitrary `PartPattern` for protocol, but in reality + the request/response server only directly receives `http` and `https` + protocols (`ws` and `wss` are upgraded from `http` and `https` respectively). + + That means params or arbitrary optionals are no longer allowed within the + protocol and will result in a `ParseError`. + +### Patch Changes + +- Add `ast` property to `RoutePattern` + + The AST is a read-only, "bare-metal" API designed for advanced use cases. For + example, optimized matchers like `TrieMatcher` can't just delegate matching to + `RoutePattern.match()` for each of their patterns and need direct access to + the pattern AST. + + ```ts + let ast: AST = pattern.ast + + type AST = { + protocol: PartPattern + hostname: PartPattern + port: string | null + pathname: PartPattern + search: SearchConstraints + } + ``` + + ```ts + type PartPattern = { + tokens: Array<Token> + paramNames: Array<string> + /** Map of `(` token index to its corresponding `)` token index for optional segments */ + optionals: Map<number, number> + separator: '.' | '/' | '' + } + + type Token = + | { type: 'text'; text: string } + | { type: 'separator' } + | { type: '(' | ')' } + | { type: ':' | '*'; nameIndex: number } // nameIndex references paramNames array + + // `posts/:id(/edit)` + let part: PartPattern = { + tokens: [ + { type: 'text', text: 'posts' }, + { type: 'separator' }, + { type: ':', nameIndex: 0 }, + { type: '(' }, + { type: 'separator' }, + { type: 'text', text: 'edit' }, + { type: ')' }, + ], + paramNames: ['id'], + optionals: new Map([[3, 6]]), // token at index 3 '(' maps to token at index 6 ')' + separator: '/', + } + ``` + + ```ts + type SearchConstraints = Map<string, Set<string> | null> + + // - `null`: key must be present (matches ?q, ?q=, ?q=1) + // - Empty Set: key must be present with a value (matches ?q=1) + // - Non-empty Set: key must be present with all these values (matches ?q=x&q=y) + ``` + +- Add getters to `RoutePattern` + + The `protocol`, `hostname`, `port`, `pathname`, and `search` getters display + the normalized pattern parts as strings. + + ```ts + let pattern = new RoutePattern( + 'https://:tenant.example.com:3000/:lang/docs/*?version=:version', + ) + + pattern.protocol // 'https' + pattern.hostname // ':tenant.example.com' + pattern.port // '3000' + pattern.pathname // ':lang/docs/*' + pattern.search // 'version=:version' + ``` + + Omitted parts return empty strings. + +- Add `meta` to match returned by `RoutePattern.match()` + + The `meta` property provides rich information about matched params (variables + and wildcards) in the hostname and pathname, analogous to RegExp + groups/indices. This enables advanced use cases that need more than just the + param values including match ranking. + + ```ts + import * as assert from 'node:assert/strict' + + let pattern = new RoutePattern('https://:tenant.example.com/:lang/docs/*') + let match = pattern.match('https://acme.example.com/en/docs/api/routes') + + assert.deepEqual(match.params, { tenant: 'acme', lang: 'en' }) + assert.deepEqual(match.meta.hostname, [ + { type: ':', name: 'tenant', value: 'acme', begin: 0, end: 4 }, + ]) + assert.deepEqual(match.meta.pathname, [ + { type: ':', name: 'lang', value: 'en', begin: 0, end: 2 }, + { type: '*', name: '*', value: 'api/routes', begin: 8, end: 18 }, + ]) + ``` + +- Add functions for comparing match specificity + + Specificity is our intuitive metric for finding the "best" match. + + ```ts + import * as Specificity from '@remix-run/route-pattern/specificity' + + Specificity.lessThan(a, b) // `true` when `a` is more specific than `b`. `false` otherwise + Specificity.greaterThan(a, b) + Specificity.equal(a, b) + + matches.sort(Specificity.ascending) + matches.sort(Specificity.descending) + ``` + + Specificity compares patterns char-by-char where static matches beat variable + matches, which beat wildcard matches. + + ```typescript + import { RoutePattern } from '@remix-run/route-pattern' + import * as Specificity from '@remix-run/route-pattern/specificity' + import * as assert from 'node:assert/strict' + + let url = 'https://example.com/posts/new' + + let pattern1 = new RoutePattern('/posts/:id') + let pattern2 = new RoutePattern('/posts/new') + + let match1 = pattern1.match(url) + let match2 = pattern2.match(url) + + assert.ok(Specificity.lessThan(match1, match2)) + ``` + + **Hostname segments are compared right-to-left** (e.g., `example.com` compares + `com` first, then `example`), though characters within a segment are still + compared left-to-right: + + ```typescript + import * as assert from 'node:assert/strict' + + let url = 'https://app-api.example.com' + + let pattern1 = new RoutePattern('https://app-*.example.com') + let match1 = pattern1.match(url) + + let pattern2 = new RoutePattern('https://*-api.example.com') + let match2 = pattern2.match(url) + + assert.ok(Specificity.lessThan(match1, match2)) + ``` + +## v0.17.0 + +### Minor Changes + +- BREAKING CHANGE: Remove exports for `TrieMatcher` and `TrieMatcherOptions` + + `TrieMatcher` prototype produces inconsistent matches based on ad hoc scoring. + That means that swapping `ArrayMatcher` for `TrieMatcher` could alter which + route was picked as the best match for a given URL. + + We'll restore the `TrieMatcher` export after it produces correct, consistent + matches. + +## v0.16.0 (2025-12-18) + +- BREAKING CHANGE: Rename `RegExpMatcher` to `ArrayMatcher` + +## v0.15.3 (2025-11-19) + +- Exclude benchmark files from published npm package + +## v0.15.2 (2025-11-19) + +- Exclude test files from published npm package + +## v0.15.1 (2025-11-19) + +- `href()` now filters out `undefined` and `null` values from search parameters, + preventing them from appearing in the generated URL's query string +- `href()` no longer adds a trailing `?` when search parameters are empty + +## v0.15.0 (2025-11-05) + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.14.0 (2025-10-04) + +- Add `Matcher` and `MatchResult` interfaces. These are new public APIs for + matching sets of patterns. +- Add `RegExpMatcher` and `TrieMatcher` concrete implementations of the + `Matcher` interface + - `RegExpMatcher` is a simple array-based matcher that compiles route patterns + to regular expressions. + - `TrieMatcher` is a trie-based matcher optimized for large route sets and + long-running server applications. + + ```tsx + import { TrieMatcher } from '@remix-run/route-pattern' + + let matcher = new TrieMatcher<{ name: string }>() + matcher.add('users/:id', { name: 'user' }) + matcher.add('posts/:id', { name: 'post' }) + + let match = matcher.match('https://example.com/users/123') + // { data: { name: 'user' }, params: { id: '123' }, url: ... } + ``` + +## v0.13.0 (2025-09-29) + +- BREAKING CHANGE: removed `createRoutes` and corresponding types (`RouteMap`, + `RouteDefs`, and `RouteDef`). This functionality will be re-introduced in a + future "router" package. +- BREAKING CHANGE: removed `RouteMap` from `createHrefBuilder` generic type. +- Expose `Join` type as public API +- Expose `HrefBuilderArgs` type as public API +- Optimization: compile patterns as needed instead of on instantiation + +## v0.12.0 (2025-09-25) + +- BREAKING CHANGE: removed `options` arg from `createHrefBuilder` +- BREAKING CHANGE: removed support for enum patterns +- Add `pattern.href(...args)` method for generating URLs from patterns + + ```tsx + import { RoutePattern } from '@remix-run/route-pattern' + + let pattern = new RoutePattern('users/:id') + pattern.href({ id: '123' }) // "/users/123" + ``` + +- Add `createRoutes` function for working with more than one pattern at a time. + This generates a `RouteMap` object that allows human-friendly naming of + patterns. + + ```tsx + import { createRoutes } from '@remix-run/route-pattern' + + let routes = createRoutes({ + home: '/', + blog: { + index: '/blog', + post: '/blog/:slug', + }, + }) + + routes.home.match('https://remix.run/') + // { params: {} } + routes.blog.post.match('https://remix.run/blog/my-post') + // { params: { slug: 'my-post' } } + + routes.blog.post.href({ slug: 'my-post' }) // "/blog/my-post" + ``` + + A `RouteMap` also works as a generic to `createHrefBuilder()` to restrict the + set of patterns that may be used as the first argument. + + ```tsx + import { createHrefBuilder } from '@remix-run/route-pattern' + + let href = createHrefBuilder<typeof routes>() + href('/blog/:slug', { slug: 'my-post' }) // "/blog/my-post" + ``` + +- Add `pattern.join(input, options)`, which allows a pattern to be built + relative to another pattern + + ```tsx + import { RoutePattern } from '@remix-run/route-pattern' + + let base = new RoutePattern('https://remix.run/api') + let pattern = base.join('users/:id') + pattern.source // "https://remix.run/api/users/:id" + ``` + +- Export `RouteMatch` type as public API +- Allow `null` and `undefined` as values for optional params + +## v0.11.0 (2025-09-11) + +- `createHrefBuilder<T>` now accepts a `RoutePattern` directly instead of just + `string`s +- `Variant<T>` preserves leading slashes in pathname-only patterns + +## v0.10.0 (2025-09-04) + +- BREAKING CHANGE: removed `match.protocol`, `match.hostname`, `match.port`, + `match.pathname`, `match.search`, and `match.searchParams`. Use `match.url` + instead +- Fix search matching and add more fine-grained examples + +## v0.9.1 (2025-09-04) + +- Fix handling of patterns with leading slash +- Make variables not greedy + +```tsx +let pattern = new RoutePattern('/:id(.json)') +// Before :id was greedy and would consume ".json" +pattern.match('https://remix.run/123.json') +// { params: { id: '123.json' } } +// After +pattern.match('https://remix.run/123.json') +// { params: { id: '123' } } +``` + +- Allow search params values to have type `string | number | bigint | boolean` + and automatically stringify + +## v0.9.0 (2025-09-03) + +- Add `protocol`, `hostname`, `port`, `pathname`, `search`, and `searchParams` + properties to the `Match` interface. This is useful to avoid parsing the URL + twice when passing a string directly to `pattern.match(urlString)` +- Fix `protocol` and `hostname` to always ignore case given in the pattern +- Add `ignoreCase` option to `RoutePattern` constructor to match URL pathnames + in a case-insensitive way + +```tsx +let pattern = new RoutePattern('https://remix.run/users/:id', { + ignoreCase: true, +}) +pattern.match('https://remix.run/Users/123') // { ..., params: { id: '123' } } +``` + +## v0.8.0 (2025-09-03) + +- Any valid pattern is also valid in `href(pattern)` +- Href generation with missing optional variables omits the optional section + entirely + +```tsx +let href = createHrefBuilder() +href('products(/:id)', { id: 'remix' }) // /products/remix + +// These all used to fail, but are now OK! +href('products(/:id)') // /products +href('products(/:id)', {}) // /products +href('products(/:id)', { id: null }) // /products (type error) +href('products(/:id)', { id: undefined }) // /products (type error) +``` + +- Param values may be `string | number | bigint | boolean` and are automatically + stringified + +```tsx +let href = createHrefBuilder() + +// These used to be a type errors, but are now OK! +href('products(/:id)', { id: 1 }) // /products/1 +href('products(/:id)', { id: false }) // /products/false +``` + +## v0.7.0 (2025-09-01) + +- Add support for nested optionals in route patterns + +```tsx +// Now you can do stuff like +let pattern = new RoutePattern('api(/v:major(.:minor))') +pattern.match('https://remix.run/api') // { params: {} } +pattern.match('https://remix.run/api/v1') // { params: { major: '1' } } +pattern.match('https://remix.run/api/v1.2') // { params: { major: '1', minor : '2' } } +``` + +- Make `pattern.match().params` type-safe +- Export top-level `Params<pattern>` helper for extracting params from a pattern +- Tighten up some types in `href()`. Now you get variants for + - all the different values of an enum + - unnamed wildcards +- Fix bug when using unnamed wildcards in `href()` + +## v0.6.0 (2025-08-29) + +- Use a single RegExp to match protocol, hostname, port, and pathname +- Allow duplicate variable names in patterns, right-most shows up in + `match.params` +- Allow route patterns to match on port +- All variables require names, wildcards may have a name or be "unnamed" + +## v0.4.0 (2025-07-24) + +- Renamed package from `@mjackson/route-pattern` to `@remix-run/route-pattern` diff --git a/docs/agents/remix/route-pattern.md b/docs/agents/remix/route-pattern/index.md similarity index 69% rename from docs/agents/remix/route-pattern.md rename to docs/agents/remix/route-pattern/index.md index a8fafa9..1b7d764 100644 --- a/docs/agents/remix/route-pattern.md +++ b/docs/agents/remix/route-pattern/index.md @@ -1,13 +1,33 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/route-pattern --> + # 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 + +## Installation -## README +```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 +43,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 +69,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 +94,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<string>() matcher.add('/', 'home') @@ -108,14 +115,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 +132,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<string>() matcher.add('blog/hello', 'static') @@ -144,32 +151,24 @@ matcher.match('https://example.com/blog/hello') let router = new ArrayMatcher<string>() 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 -To run benchmarks comparing `route-pattern` performance with comparable -libraries: - -```sh -pnpm bench bench/comparison.bench.ts -``` +Benchmarks live in +[`bench/`](https://github.com/remix-run/remix/tree/remix%403.0.0-alpha.6/packages/route-pattern/bench). ## Related Work - [`path-to-regexp`](https://www.npmjs.com/package/path-to-regexp) +- [`find-my-way`](https://github.com/delvedor/find-my-way) - [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) ## 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/changelog.md b/docs/agents/remix/session-middleware/changelog.md new file mode 100644 index 0000000..ee207c5 --- /dev/null +++ b/docs/agents/remix/session-middleware/changelog.md @@ -0,0 +1,66 @@ +# `session-middleware` CHANGELOG + +This is the changelog for +[`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.2.1 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.2.0 + +### Minor Changes + +- BREAKING CHANGE: Session middleware no longer reads/writes `context.session`. + + Session state is now stored on request context using the `Session` class + itself as the context key and accessed with `context.get(Session)`. + +- `session()` now contributes `Session` to `fetch-router`'s typed request + context, so apps deriving context from middleware can read + `context.get(Session)` without manual type assertions. + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.1.4 + +### Patch Changes + +- Ensure response is mutable before modifying. + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + +## v0.1.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + +## v0.1.2 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.1.1 (2025-12-06) + +- Use `response.headers.append('Set-Cookie', ...)` instead of + `response.headers.set('Set-Cookie', ...)` to not overwrite cookies set by + other middleware/handlers + +## v0.1.0 (2025-11-19) + +Initial release extracted from `@remix-run/fetch-router` v0.9.0. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/session-middleware/README.md) +for more details. diff --git a/docs/agents/remix/session-middleware.md b/docs/agents/remix/session-middleware/index.md similarity index 60% rename from docs/agents/remix/session-middleware.md rename to docs/agents/remix/session-middleware/index.md index 70a1d76..6535cc8 100644 --- a/docs/agents/remix/session-middleware.md +++ b/docs/agents/remix/session-middleware/index.md @@ -1,26 +1,31 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/session-middleware --> + # 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 +41,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 +61,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(` <html> @@ -74,7 +82,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 +100,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 +119,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/changelog.md b/docs/agents/remix/session-storage-memcache/changelog.md new file mode 100644 index 0000000..6765a4a --- /dev/null +++ b/docs/agents/remix/session-storage-memcache/changelog.md @@ -0,0 +1,22 @@ +# `session-storage-memcache` CHANGELOG + +This is the changelog for +[`session-storage-memcache`](https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache). +It follows [semantic versioning](https://semver.org/). + +## v0.1.0 + +### Minor Changes + +- Add Memcache session storage with + `createMemcacheSessionStorage(server, options)`. + + This adds a Node.js Memcache backend with support for `useUnknownIds`, + `keyPrefix`, and `ttlSeconds`, along with integration tests that run against + Memcached in CI. + +## Unreleased + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/session-storage-memcache/index.md b/docs/agents/remix/session-storage-memcache/index.md new file mode 100644 index 0000000..5d89214 --- /dev/null +++ b/docs/agents/remix/session-storage-memcache/index.md @@ -0,0 +1,44 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/session-storage-memcache --> + +# 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.md deleted file mode 100644 index 96534c4..0000000 --- a/docs/agents/remix/session-storage-redis.md +++ /dev/null @@ -1,43 +0,0 @@ -# session-storage-redis - -Source: -https://github.com/remix-run/remix/tree/main/packages/session-storage-redis - -## README - -Redis-backed session storage for `remix/session`. - -## Installation - -```sh -npm i remix redis -``` - -## Usage - -```ts -import { createClient } from 'redis' -import { createRedisSessionStorage } from 'remix/session-storage-redis' - -let redis = createClient({ url: process.env.REDIS_URL }) -await redis.connect() - -let sessionStorage = createRedisSessionStorage(redis, { - keyPrefix: 'session:', - ttl: 60 * 60 * 24, -}) -``` - -## Options - -- `keyPrefix` (`string`, default: `'session:'`) -- `ttl` (`number` in 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-storage-redis/changelog.md b/docs/agents/remix/session-storage-redis/changelog.md new file mode 100644 index 0000000..a7453a2 --- /dev/null +++ b/docs/agents/remix/session-storage-redis/changelog.md @@ -0,0 +1,18 @@ +# `session-storage-redis` CHANGELOG + +This is the changelog for +[`session-storage-redis`](https://github.com/remix-run/remix/tree/main/packages/session-storage-redis). +It follows [semantic versioning](https://semver.org/). + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/session-storage-redis` with + `createRedisSessionStorage()`. + +## Unreleased + +### Minor Changes + +- Initial release. diff --git a/docs/agents/remix/session-storage-redis/index.md b/docs/agents/remix/session-storage-redis/index.md new file mode 100644 index 0000000..1b297d9 --- /dev/null +++ b/docs/agents/remix/session-storage-redis/index.md @@ -0,0 +1,40 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/session-storage-redis --> + +# session-storage-redis + +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-run/session @remix-run/session-storage-redis redis +``` + +## Usage + +```ts +import { createClient } from 'redis' +import { createRedisSessionStorage } from '@remix-run/session-storage-redis' + +let redis = createClient({ url: process.env.REDIS_URL }) +await redis.connect() + +let sessionStorage = createRedisSessionStorage(redis, { + keyPrefix: 'session:', + ttl: 60 * 60 * 24, +}) +``` + +## Options + +`createRedisSessionStorage(client, options)` supports: + +- `keyPrefix` (`string`, default: `'session:'`) +- `ttl` (`number` seconds) +- `useUnknownIds` (`boolean`, default: `false`) + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/session/changelog.md b/docs/agents/remix/session/changelog.md new file mode 100644 index 0000000..b72b3b4 --- /dev/null +++ b/docs/agents/remix/session/changelog.md @@ -0,0 +1,80 @@ +# `session` CHANGELOG + +This is the changelog for +[`session`](https://github.com/remix-run/remix/tree/main/packages/session). It +follows [semantic versioning](https://semver.org/). + +## v0.4.1 (2025-12-06) + +- Always delete the original session ID when it is regenerated with the + `deleteOldSession` option. Intermediate IDs are never saved to storage, so + they can't be deleted. + +## v0.4.0 (2025-11-25) + +- Add `Session` class. The `createSession` function now returns an instance of + the `Session` class. + + ```ts + // You can now create sessions using either approach: + import { createSession, Session } from '@remix-run/session' + + // Factory function + let session = createSession() + + // Or use the class directly + let session = new Session() + ``` + +- BREAKING CHANGE: Rename `createFileSessionStorage` to `createFsSessionStorage` + and export from `@remix-run/session/fs-storage` + + ```ts + // before + import { createFileSessionStorage } from '@remix-run/session/file-storage' + let storage = createFileSessionStorage('/tmp/sessions') + + // after + import { createFsSessionStorage } from '@remix-run/session/fs-storage' + let storage = createFsSessionStorage('/tmp/sessions') + ``` + +## v0.3.0 (2025-11-21) + +- BREAKING CHANGE: Rename `createFileStorage` to `createFileSessionStorage` +- BREAKING CHANGE: Rename `createMemoryStorage` to `createMemorySessionStorage` +- BREAKING CHANGE: Rename `createCookieStorage` to `createCookieSessionStorage` + +## v0.2.1 (2025-11-19) + +- Fix flash messages persisting across multiple requests. Flash data is now + automatically cleared after being available for one request, even if the + session is not otherwise modified + +## v0.2.0 (2025-11-18) + +- BREAKING CHANGE: Remove `Session` class, use `createSession` instead +- BREAKING CHANGE: Remove class versions of session storage, use the factory + functions instead + + ```tsx + // before + import { FileSessionStorage } from '@remix-run/session/file-storage' + let storage = new FileSessionStorage(/* ... */) + + // after + import { createFileStorage } from '@remix-run/session/file-storage' + let storage = createFileStorage(/* ... */) + ``` + +- Add `session.regenerateId(deleteOldSession?: boolean)` to purge old session + data when the session ID is regenerated. This is useful for preventing session + fixation attacks. + +## v0.1.0 (2025-11-08) + +This is the initial release of `@remix-run/session`. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/session/README.md) +for more details. 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 index 6e758ad..4292f30 100644 --- a/docs/agents/remix/session/index.md +++ b/docs/agents/remix/session/index.md @@ -1,34 +1,37 @@ -# session - -Source: https://github.com/remix-run/remix/tree/main/packages/session +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/session --> -## Overview +# session -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. +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 - 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 + [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 -bun add @remix-run/session +npm i remix ``` ## 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. +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-run/session/cookie-storage' +import { createCookieSessionStorage } from 'remix/session/cookie-storage' // Create a session storage. This is used to store session data across requests. let storage = createCookieSessionStorage() @@ -59,9 +62,145 @@ 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 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 -- [Flash data and security](./flash-and-security.md) -- [Storage strategies](./storage-strategies.md) -- [Related packages](./related.md) -- [Remix package index](../index.md) +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) 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/changelog.md b/docs/agents/remix/static-middleware/changelog.md new file mode 100644 index 0000000..5a243b5 --- /dev/null +++ b/docs/agents/remix/static-middleware/changelog.md @@ -0,0 +1,124 @@ +# `static-middleware` CHANGELOG + +This is the changelog for +[`static-middleware`](https://github.com/remix-run/remix/tree/main/packages/static-middleware). +It follows [semantic versioning](https://semver.org/). + +## v0.4.7 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fs@0.4.3`](https://github.com/remix-run/remix/releases/tag/fs@0.4.3) + - [`mime@0.4.1`](https://github.com/remix-run/remix/releases/tag/mime@0.4.1) + - [`response@0.3.3`](https://github.com/remix-run/remix/releases/tag/response@0.3.3) + +## v0.4.6 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## v0.4.5 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.18.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.0) + +## v0.4.4 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0) + - [`fs@0.4.2`](https://github.com/remix-run/remix/releases/tag/fs@0.4.2) + - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0) + - [`response@0.3.2`](https://github.com/remix-run/remix/releases/tag/response@0.3.2) + +## v0.4.3 + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0) + +## v0.4.2 + +### Patch Changes + +- Changed `@remix-run/*` peer dependencies to regular dependencies + +## v0.4.1 + +### Patch Changes + +- Update `@remix-run/fs` peer dependency to use new `openLazyFile()` API + +## v0.4.0 (2025-11-25) + +- BREAKING CHANGE: Replace `mrmime` dependency with `@remix-run/mime` for MIME + type detection which is now a peer dependency. + +- Add support for `acceptRanges` function to conditionally enable HTTP Range + requests based on the file being served: + + ```ts + // Enable ranges only for large files + staticFiles('./public', { + acceptRanges: (file) => file.size > 10 * 1024 * 1024, + }) + + // Enable ranges only for videos + staticFiles('./public', { + acceptRanges: (file) => file.type.startsWith('video/'), + }) + ``` + +## v0.3.0 (2025-11-25) + +- BREAKING CHANGE: Now uses `@remix-run/response` for file and HTML responses + instead of `@remix-run/fetch-router/response-helpers`. The + `@remix-run/response` package is now a peer dependency. +- Add `listFiles` option to generate a directory listing when a directory is + requested. + + ```ts + staticFiles('./public', { listFiles: true }) + ``` + +## v0.2.0 (2025-11-20) + +- Read the request method from `context.method` instead of + `context.request.method`, so it's compatible with the + [`method-override` middleware](https://github.com/remix-run/remix/tree/main/packages/method-override-middleware) +- Add `@remix-run/fs` as a peer dependency. This package now imports from + `@remix-run/fs` instead of `@remix-run/lazy-file/fs`. +- Add `index` option to configure which files to serve when a directory is + requested. When a request targets a directory, the middleware will try each + index file in order until one is found. Defaults to + `['index.html', 'index.htm']`. Supports boolean shortcuts: `true` for + defaults, `false` to disable. + + ```ts + // Serve index.html from directories by default + staticFiles('./public') + + // Custom index files + staticFiles('./public', { + index: ['default.html', 'home.html'], + }) + + // Disable index file serving + staticFiles('./public', { index: false }) + staticFiles('./public', { index: [] }) + ``` + +## v0.1.0 (2025-11-19) + +Initial release extracted from `@remix-run/fetch-router` v0.9.0. + +See the +[README](https://github.com/remix-run/remix/blob/main/packages/static-middleware/README.md) +for more details. diff --git a/docs/agents/remix/static-middleware.md b/docs/agents/remix/static-middleware/index.md similarity index 61% rename from docs/agents/remix/static-middleware.md rename to docs/agents/remix/static-middleware/index.md index c5f9ac6..1978c0a 100644 --- a/docs/agents/remix/static-middleware.md +++ b/docs/agents/remix/static-middleware/index.md @@ -1,30 +1,25 @@ -# static-middleware - -Source: https://github.com/remix-run/remix/tree/main/packages/static-middleware - -## README +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/static-middleware --> -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). +# static-middleware -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 +27,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')], @@ -45,7 +40,7 @@ router.get('/', () => new Response('Home')) ### With Cache Control Internally, the `staticFiles()` middleware uses the -[`createFileResponse()` helper from `@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response/README.md#file-responses) +[`createFileResponse()` helper from `@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response/index.md#file-responses) to send files with full HTTP semantics. This means it also accepts the same options as the `createFileResponse()` helper. @@ -103,7 +98,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/changelog.md b/docs/agents/remix/tar-parser/changelog.md new file mode 100644 index 0000000..1eff3ca --- /dev/null +++ b/docs/agents/remix/tar-parser/changelog.md @@ -0,0 +1,55 @@ +# `tar-parser` CHANGELOG + +This is the changelog for +[`tar-parser`](https://github.com/remix-run/remix/tree/main/packages/tar-parser). +It follows [semantic versioning](https://semver.org/). + +## v0.7.1 + +### Patch Changes + +- Fix parsing tar entries whose file body ends exactly at a chunk boundary. + +## v0.7.0 (2025-11-20) + +- Update dev dependencies to use `@remix-run/fs` instead of + `@remix-run/lazy-file/fs`. + +## v0.6.0 (2025-11-04) + +- Build using `tsc` instead of `esbuild`. This means modules in the `dist` + directory now mirror the layout of modules in the `src` directory. + +## v0.5.0 (2025-10-22) + +- BREAKING CHANGE: Removed CommonJS build. This package is now ESM-only. If you + need to use this package in a CommonJS project, you will need to use dynamic + `import()`. + +## v0.4.0 (2025-07-24) + +- Renamed package from `@mjackson/tar-parser` to `@remix-run/tar-parser` + +## v0.3.0 (2025-06-06) + +- Add `/src` to npm package, so "go to definition" goes to the actual source +- Use one set of types for all built files, instead of separate types for ESM + and CJS +- Build using esbuild directly instead of tsup + +## v0.2.2 (2025-02-04) + +- Add `Promise<void>` to `TarEntryHandler` return type + +## v0.2.1 (2025-01-24) + +- Add support for environments that do not support + `ReadableStream.prototype[Symbol.asyncIterator]` (i.e. Safari), see #46 + +## v0.2.0 (2025-01-07) + +- Fix a bug that hangs the process when trying to read zero-length entries. + +## v0.1.0 (2024-12-06) + +- Initial release diff --git a/docs/agents/remix/tar-parser.md b/docs/agents/remix/tar-parser/index.md similarity index 62% rename from docs/agents/remix/tar-parser.md rename to docs/agents/remix/tar-parser/index.md index 9f0b68d..5dc7c33 100644 --- a/docs/agents/remix/tar-parser.md +++ b/docs/agents/remix/tar-parser/index.md @@ -1,21 +1,10 @@ -# 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](<https://en.wikipedia.org/wiki/Tar_(computing)>). +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/tar-parser --> -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 -`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](<https://en.wikipedia.org/wiki/Tar_(computing)>) 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 +18,8 @@ all runtimes. ## Installation -Install from [npm](https://www.npmjs.com/): - ```sh -bun add @remix-run/tar-parser +npm i remix ``` ## Usage @@ -40,7 +27,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 +59,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 +88,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/docs/agents/remix/terminal/changelog.md b/docs/agents/remix/terminal/changelog.md new file mode 100644 index 0000000..af250c9 --- /dev/null +++ b/docs/agents/remix/terminal/changelog.md @@ -0,0 +1,19 @@ +# `terminal` CHANGELOG + +This is the changelog for +[`terminal`](https://github.com/remix-run/remix/tree/main/packages/terminal). It +follows [semantic versioning](https://semver.org/). + +## v0.1.0 + +### Minor Changes + +- Initial release of terminal output utilities for ANSI styles, color capability + detection, escape sequences, and testable terminal streams. Automatic color + detection disables styles for CI, `NO_COLOR`, `TERM=dumb`, and non-TTY output + streams by default, and can be overridden with the `colors` option. Style + helpers include common modifiers, foreground colors, background colors, bright + variants, and preserve outer styles when nested formatted strings close inner + styles. + +## Unreleased diff --git a/docs/agents/remix/terminal/index.md b/docs/agents/remix/terminal/index.md new file mode 100644 index 0000000..02252fc --- /dev/null +++ b/docs/agents/remix/terminal/index.md @@ -0,0 +1,107 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/terminal --> + +# terminal + +Terminal output utilities for JavaScript libraries and CLIs. It provides small +primitives for ANSI styles, color support detection, escape sequences, and +testable stdout/stderr handling. + +## Features + +- **ANSI Styles** - Apply common modifiers, foreground colors, and background + colors +- **Color Detection** - Respect `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, + TTY streams, and explicit style overrides +- **Terminal Controls** - Generate escape sequences for cursor movement, line + clearing, and cursor visibility +- **Testable Streams** - Create terminal instances around injected + stdout/stderr/stdin streams + +## Installation + +```sh +npm i remix +``` + +## Usage + +```ts +import { createTerminal } from 'remix/terminal' + +let terminal = createTerminal() + +terminal.writeLine(`${terminal.styles.green('ready')} listening on port 3000`) +terminal.errorLine(terminal.styles.red('failed to start')) +``` + +### ANSI Styles + +Use `createStyles` when you only need formatting helpers. + +```ts +import { createStyles } from 'remix/terminal' + +let styles = createStyles({ colors: true }) + +console.log(styles.bold(styles.cyan('Ready'))) +console.log(styles.format('warning', 'dim', 'yellow', 'bgBlackBright')) +``` + +Style helpers preserve outer styles when nested formatted strings close an inner +style. + +```ts +console.log(styles.red(`Error: ${styles.bold('fatal')} retrying`)) +``` + +Supported modifiers include `bold`, `dim`, `italic`, `underline`, `overline`, +`inverse`, and `strikethrough`. Supported colors include the base +foreground/background ANSI colors, bright variants, and `gray`/`grey` aliases. + +By default, color detection disables styles in CI, when `NO_COLOR` is present, +for `TERM=dumb`, and outside TTY output streams. Set `colors` to `true` or +`false` to override automatic detection. + +### Terminal Controls + +Use `ansi` for raw terminal escape sequences. + +```ts +import { ansi } from 'remix/terminal' + +process.stdout.write(ansi.clearLine) +process.stdout.write(ansi.cursorTo(0)) +process.stdout.write('Updated status') +``` + +### Testing Output + +Inject streams to test terminal output without writing to the real console. + +```ts +import { createTerminal } from 'remix/terminal' + +let output = '' + +let terminal = createTerminal({ + colors: false, + stdout: { + write(chunk) { + output += chunk + }, + }, +}) + +terminal.writeLine(terminal.styles.green('ok')) +``` + +## Related Packages + +- [`logger-middleware`](https://github.com/remix-run/remix/tree/main/packages/logger-middleware) - + HTTP request/response logging middleware +- [`test`](https://github.com/remix-run/remix/tree/main/packages/test) - + Browser-based test framework for Remix components + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/test/changelog.md b/docs/agents/remix/test/changelog.md new file mode 100644 index 0000000..55ac94b --- /dev/null +++ b/docs/agents/remix/test/changelog.md @@ -0,0 +1,69 @@ +# test changelog + +## v0.2.0 + +### Minor Changes + +- Add `glob.exclude` config for filtering paths during test discovery (defaults + to `node_modules/**`) + +- Add code coverage reporting to `remix-test` + - You can enable coverage with default settings vis `remix-test --coverage` or + setting `coverage:true` in your `remix-test.config.ts` + - Or you can specify individual coverage settings via the following config + fields: + - `coverage.dir`: Directory to store coverage information (default + `.coverage`) + - `coverage.include`: Array of globs for files to include in coverage + - `coverage.exclude`: Array of globs for files to exclude from coverage + - `coverage.statements`: Percentage threshold for statement coverage + - `coverage.lines`: Percentage threshold for line coverage + - `coverage.branches`: Percentage threshold for branch coverage + - `coverage.functions`: Percentage threshold for function coverage + +- Export `runRemixTest` from `@remix-run/test/cli` so other tools can run the + Remix test runner programmatically without exiting the host process. The + function returns an exit code so callers can decide how to terminate. The + `remix-test` executable now declares Node.js 24.3.0 or later in package + metadata. + +### Patch Changes + +- Internal refactor to test discovery to better support test execution in `bun`. + - Unlike Node, Bun's `fs.promises.glob` _follows_ symbolic links and does not + prune traversal via the `exclude` option, which can cause the test runner to + enter `node_modules` symlink cycles in pnpm workspaces + - Refactored the internal test discovery logic to detect and use Bun's native + `Glob` class when running under the Bun runtime. Bun's `Glob#scan` does not + follow symlinks by default, avoiding the cycle. + - The Node runtime continues to use `fs.promises.glob` + +- Use native dynamic `import()` in Bun to load `.ts` and `.tsx` files in the + test runner + +- Bumped `@remix-run/*` dependencies: + - [`terminal@0.1.0`](https://github.com/remix-run/remix/releases/tag/terminal@0.1.0) + +## v0.1.0 + +### Minor Changes + +- Initial release of `@remix-run/test`, a test framework for Remix applications. + - `describe`/`it` test structure with + `before`/`after`/`beforeEach`/`afterEach` hooks + - `TestContext` (`t`) per test: `t.mock.fn()`, `t.mock.method()`, `t.after()` + for cleanup + - Playwright E2E testing via `t.serve()` + - CLI (`remix-test`) with flags for all config options + - Watch mode (`--watch`) + - Config file support (`remix-test.config.ts`) + - `globalSetup`/`globalTeardown` hooks via the `setup` module, called once + before/after the entire test run + +### Patch Changes + +- Bumped `@remix-run/*` dependencies: + - [`component@0.7.0`](https://github.com/remix-run/remix/releases/tag/component@0.7.0) + - [`fetch-router@0.18.1`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.18.1) + +## Unreleased diff --git a/docs/agents/remix/test/index.md b/docs/agents/remix/test/index.md new file mode 100644 index 0000000..229b77f --- /dev/null +++ b/docs/agents/remix/test/index.md @@ -0,0 +1,468 @@ +<!-- Downloaded from https://github.com/remix-run/remix/tree/remix@3.0.0-alpha.6/packages/test --> + +# `test` + +A test framework for JavaScript and TypeScript projects. + +## Features + +- `describe`/`it` test structure with `before`/`after`/`beforeEach`/`afterEach` + hooks +- Server-side unit testing +- Playwright E2E testing via `t.serve` +- In-browser component testing (pair with `render` from `remix/ui/test`) +- Mock functions and method spies via `t.mock.fn` / `t.mock.method` +- Unified code coverage reporting across unit and E2E tests +- Watch mode +- Config file support (`remix-test.config.ts`) + +## Installation + +```sh +npm i remix +``` + +## Usage + +Write test files that import from `remix/test`: + +```ts +import * as assert from 'remix/assert' +import { describe, it } from 'remix/test' + +describe('My Test Suite', () => { + it('tests a function', () => { + let result = something() + assert.equal(result, 42) + }) +}) +``` + +Run tests with the CLI: + +```sh +remix test +``` + +By default, `remix test` discovers all files matching +`**/*.test{,.e2e}.{ts,tsx}`. Pass a glob as the first positional argument to +override: + +```sh +remix test "src/**/*.test.ts" +``` + +Or, you may control via the `glob.test` config field/CLI arg. + +If you install `@remix-run/test` directly instead of the umbrella `remix` +package, the same runner is available as `remix-test`: + +```sh +npm i @remix-run/test +remix-test +``` + +### Config File + +Create a `remix-test.config.ts` (or `.js`) file at the root of your project +(shown with default values): + +```ts +import type { RemixTestConfig } from 'remix/test' + +export default { + // Browser options for E2E tests + browser: { + // Echo browser console output to the terminal + echo: false, + // Open browser (via playwright `headless:false`) and keep it open after tests + // complete (useful for debugging) + open: false, + }, + + // Max number of concurrent test workers (default `os.availableParallelism()`) + concurrency: 2, + + // Code coverage options + coverage: { + // Enable coverage reporting + enabled: true, + // Output directory (default: ".coverage") + dir: '.coverage', + // Glob patterns to include/exclude + include: ['src/**'], + exclude: ['src/**/*.test.ts'], + // Minimum thresholds (%) + statements: 80, + lines: 80, + branches: 80, + functions: 80, + }, + + glob: { + // Glob pattern identifying all test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}") + test: '**/*.test{,.browser,.e2e}.ts', + // Glob pattern identifying browser test files (default: "**/*.test.browser.{ts,tsx}") + browser: '**/*.test.browser.ts', + // Glob pattern identifying E2E test files (default: "**/*.test.e2e.{ts,tsx}") + e2e: '**/*.test.e2e.ts', + }, + + // Playwright configuration for E2E tests, or string path to an existing + // config file on disk + playwrightConfig: { + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + ], + use: { + navigationTimeout: 5_000, + actionTimeout: 5_000, + }, + }, + + // Comma-separated list of playwright projects to run E2E tests for + project: 'chromium', + + // Test reporter ("spec", "files", "tap", "dot") + reporter: 'spec', + + // Path to a setup module (see Setup section below) + setup: './test/setup.ts', + + // Comma-separated list of test types to run ("server", "browser", "e2e") + type: 'server,browser,e2e', + + // Watch for file changes and re-run + watch: false, +} satisfies RemixTestConfig +``` + +### CLI Options + +You can point to a different config file location with the `--config` flag: + +```sh +remix test --config ./tests/config.ts +``` + +You may also specify any config field as a CLI flag which will take precedence +over config file values: + +| Flag | Short | +| --------------------------- | ----- | +| `--browser.echo` | | +| `--browser.open` | | +| `--concurrency <n>` | `-c` | +| `--coverage` | | +| `--coverage.dir <path>` | | +| `--coverage.include` | | +| `--coverage.exclude` | | +| `--coverage.statements` | | +| `--coverage.lines` | | +| `--coverage.branches` | | +| `--coverage.functions` | | +| `--glob.test` | | +| `--glob.browser` | | +| `--glob.e2e` | | +| `--playwrightConfig <path>` | | +| `--project <name>` | `-p` | +| `--reporter <name>` | `-r` | +| `--setup <path>` | `-s` | +| `--type <name>` | `-t` | +| `--watch` | `-w` | + +### Setup + +The `setup` option points to a module that can export `globalSetup` and/or +`globalTeardown` functions, called once before and after the entire test run +respectively: + +```ts +// ./test/setup.ts +export async function globalSetup() { + await db.migrate() +} + +export async function globalTeardown() { + await db.close() +} +``` + +## API + +### Test framework + +```ts +import { + beforeAll, + afterAll, + beforeEach, + afterEach, + describe, + it, +} from 'remix/test' + +beforeAll(() => {}) +afterAll(() => {}) + +describe('My Test Suite', () => { + beforeEach(() => {}) + afterEach(() => {}) + + it('tests something', () => {}) + it('tests something else', () => {}) +}) +``` + +`suite` and `test` are aliases for `describe` and `it`. + +```ts +import { suite, test } from 'remix/test' + +suite('My Test Suite', () => { + test('tests something', () => {}) +}) +``` + +### Programmatic runner + +`@remix-run/test/cli` exports `runRemixTest()` for tools that want to run the +test runner without exiting the current process: + +```ts +import { runRemixTest } from '@remix-run/test/cli' + +let exitCode = await runRemixTest({ + argv: ['--type', 'server'], + cwd: process.cwd(), +}) +``` + +`runRemixTest()` returns the runner exit code. The `remix test` and `remix-test` +bin wrappers call `process.exit()` with that code when the run finishes so open +workers, browsers, or project handles cannot keep the CLI alive. + +### Test Context + +Each test callback receives a `TestContext` (`t`) as its first argument with +helpful test utilities. + +```ts +// from 'remix/test' +interface TestContext { + // Register a cleanup function to run after the test completes + after(fn: () => void): void + + // Mock tracker, mirroring the shape of Node's `t.mock` from `node:test` + mock: { + // Create a mock function with an optional implementation + fn<T extends (...args: any[]) => any>(impl?: T): MockFunction<T> + + // Mock an object method with an optional implementation override + method<T extends object, K extends keyof T>( + obj: T, + methodName: K, + impl?: Function, + ): MockFunction + } + + // Replace global timer functions with controllable fakes + useFakeTimers(): FakeTimers + + // E2E only: connect a running test server to a Playwright Page + serve(server: { baseUrl: string; close(): Promise<void> }): Promise<Page> +} +``` + +#### Mocks and Spies + +Use `t.mock.fn()`/`t.mock.method()` to set up mocks and method spies. This is +preferred over the standalone `mock` import because TestContext method mocks are +automatically restored after the test runs. + +```ts +it('mocks and spies', (t) => { + // Create a mock function + let fn = t.mock.fn((x: number) => x * 2) + fn(3) + fn.mock.calls[0].result // 6 + + // Mock an existing method + let spy = t.mock.method(console, 'warn') + console.warn('test') + spy.mock.calls.length // 1 + // spy is restored automatically when the test ends +}) +``` + +#### Cleanup + +You can register local test cleanup logic with `t.after()`: + +```ts +it('cleanup', (t) => { + let conn = db.connect() + t.after(() => conn.close()) + // ... +}) +``` + +#### Fake Timers + +`t.useFakeTimers()` replaces the global timer functions (`setTimeout`, +`setInterval`, etc.) with controllable fakes that are automatically restored +after the test. It works in any test environment — server unit tests, browser +tests, or E2E setup code. + +```ts +it('debounces a callback', (t) => { + let timers = t.useFakeTimers() + let calls = 0 + let debounced = debounce(() => calls++, 300) + + debounced() + timers.advance(299) + assert.equal(calls, 0) + timers.advance(1) + assert.equal(calls, 1) +}) +``` + +| Method | Description | +| ------------- | --------------------------------------------------------------------------- | +| `advance(ms)` | Advance the clock by `ms` milliseconds, firing any elapsed timers | +| `restore()` | Restore the original timer functions (called automatically after each test) | + +#### E2E + +In E2E test files, `t.serve()` connects a running test server to a Playwright +`Page`. See [E2E Testing](#e2e-testing) for details. + +```ts +import { createTestServer } from 'remix/node-fetch-server/test' + +it('navigates to home', async (t) => { + let router = createRouter() + let server = await createTestServer(router.fetch) + let page = await t.serve(server) + await page.goto('/') +}) +``` + +### Standalone mocks (module scope) + +When you need a mock outside of a test body, import `mock` directly and call +`restore()` manually: + +```ts +import { mock } from 'remix/test' + +let spy = mock.method(console, 'log') +// ... +spy.mock.restore?.() +``` + +### Browser Testing + +Browser tests run components in an actual browser environment via Playwright and +are discovered by the `**/*.test.browser.{ts,tsx}` glob pattern (configurable +via `glob.browser`). They use the same `describe`/`it` API as unit tests. Each +in-browser test suite runs in an isolated `iframe` so it has access to its own +`document` instance. + +#### `render()` + +`render`, exported from `remix/ui/test`, mounts a component into the DOM and +returns a `RenderResult`: + +```ts +import * as assert from 'remix/assert' +import { describe, it } from 'remix/test' +import { render } from 'remix/ui/test' +import { Counter } from './counter.tsx' + +describe('Counter', () => { + it('increments on click', async (t) => { + let { $, act, cleanup } = render(<Counter />) + t.after(cleanup) + + assert.equal($('[data-count]')?.textContent, '0') + await act(() => $('[data-action="increment"]')?.click()) + assert.equal($('[data-count]')?.textContent, '1') + }) +}) +``` + +`RenderResult` provides: + +| Property/Method | Description | +| --------------- | ----------------------------------------------------------------------- | +| `container` | The `HTMLElement` the component is mounted into | +| `root` | The Remix `VirtualRoot` the component is rendered in | +| `$(selector)` | Alias for `container.querySelector()` | +| `$$(selector)` | Alias for `container.querySelectorAll()` | +| `act(fn)` | Runs `fn` and flushes pending component updates | +| `cleanup()` | Unmounts and removes the container (pass to `t.after` for auto-cleanup) | + +### E2E Testing + +End-to-end (E2E) tests use [Playwright](https://playwright.dev) and are +discovered by the `**/*.test.e2e.{ts,tsx}` glob pattern (configurable via +`glob.e2e`). They use the same `describe`/`it` API as unit tests. + +E2E tests receive `t.serve()` on the test context, which accepts a running test +server and returns a Playwright +[`Page`](https://playwright.dev/docs/api/class-page) whose `baseURL` points at +that server. The server and page are automatically closed after each test. + +```ts +import * as assert from 'remix/assert' +import { createTestServer } from 'remix/node-fetch-server/test' +import { describe, it } from 'remix/test' +import { createRouter } from './router.ts' + +describe('checkout', () => { + it('adds an item to the cart', async (t) => { + let router = createRouter() + let server = await createTestServer(router.fetch) + let page = await t.serve(server) + + await page.goto('/') + await page.getByRole('button', { name: 'Add to Cart' }).click() + await page.getByRole('link', { name: 'Cart' }).click() + await page.getByRole('heading', { name: 'Shopping Cart' }).waitFor() + + assert.equal(await page.locator('[data-test-cart-quantity]').innerText(), 1) + }) +}) +``` + +Configure Playwright (browsers, timeouts, viewport, etc.) via `playwrightConfig` +in your config file: + +```ts +export default { + playwrightConfig: { + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + { name: 'webkit', use: { browserName: 'webkit' } }, + ], + use: { + navigationTimeout: 5_000, + actionTimeout: 5_000, + }, + }, + + // Or, point to an existing playwright config file + // playwrightConfig: './playwright.config.ts' +} satisfies RemixTestConfig +``` + +Set `browser.open: true` to keep the browser open after tests finish — useful +for debugging failures. + +## License + +See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/ui/changelog.md b/docs/agents/remix/ui/changelog.md new file mode 100644 index 0000000..a843e1f --- /dev/null +++ b/docs/agents/remix/ui/changelog.md @@ -0,0 +1,106 @@ +# `theme` CHANGELOG + +This is the changelog for +[`ui`](https://github.com/remix-run/remix/tree/main/packages/ui). It follows +[semantic versioning](https://semver.org/). + +## v0.1.0 + +### Minor Changes + +- BREAKING CHANGE: Consolidated the deprecated `@remix-run/component` package + into `@remix-run/ui`. Import component runtime APIs from `@remix-run/ui`, + server rendering APIs from `@remix-run/ui/server`, JSX runtime APIs from + `@remix-run/ui/jsx-runtime` and `@remix-run/ui/jsx-dev-runtime`, and animation + APIs from `@remix-run/ui/animation`. + + Removed the deprecated `@remix-run/ui/on-outside-pointer-down` export. Use the + popover, menu, or other component-level outside interaction APIs instead. + +- BREAKING CHANGE: Components now receive props through a stable `handle.props` + object using `Handle<Props, Context>` instead of receiving a separate `setup` + argument and render callback props. Move initialization values that previously + used `<Component setup={...} />` onto regular props, and read all props from + `handle.props` in both the component function and render callback. + + Before: + + ```tsx + function Counter( + handle: Handle<CounterContext>, + setup: { initialCount: number }, + ) { + let count = setup.initialCount + + return (props: { label: string }) => ( + <button> + {props.label}: {count} + </button> + ) + } + + ;<Counter setup={{ initialCount: 10 }} label="Count" /> + ``` + + After: + + ```tsx + function Counter( + handle: Handle<{ initialCount: number; label: string }, CounterContext>, + ) { + let count = handle.props.initialCount + + return () => ( + <button> + {handle.props.label}: {count} + </button> + ) + } + + ;<Counter initialCount={10} label="Count" /> + ``` + + The `handle.props` object keeps the same identity for the component lifetime + while its values are updated before each render, so destructuring + `let { props, update } = handle` remains safe. The `setup` prop is no longer + special and is treated like any other prop. + + This also removes the old pattern where setup-scope helpers had to read from a + mutable variable that was reassigned inside the render callback: + + ```tsx + function Listbox(handle: Handle<ListboxContext>) { + let props: ListboxProps + + function select(value: string) { + props.onSelect(value) + } + + handle.context.set({ select }) + + return (nextProps: ListboxProps) => { + props = nextProps + return props.children + } + } + ``` + + Helpers can now read the current props directly from the stable handle: + + ```tsx + function Listbox(handle: Handle<ListboxProps, ListboxContext>) { + function select(value: string) { + handle.props.onSelect(value) + } + + handle.context.set({ select }) + + return () => handle.props.children + } + ``` + +- BREAKING CHANGE: Removed the deprecated `keysEvents`, `pressEvents`, and + `PressEvent` exports from `@remix-run/ui`. Use `on(...)` with native DOM + keyboard, pointer, and click events directly instead. + +## Unreleased diff --git a/docs/agents/remix/ui/docs/component-changelog.md b/docs/agents/remix/ui/docs/component-changelog.md new file mode 100644 index 0000000..045e413 --- /dev/null +++ b/docs/agents/remix/ui/docs/component-changelog.md @@ -0,0 +1,821 @@ +# `component` CHANGELOG + +This is the changelog for +[`component`](https://github.com/remix-run/remix/tree/main/packages/ui). It +follows [semantic versioning](https://semver.org/). + +## v0.7.0 + +### Minor Changes + +- BREAKING CHANGE: mixin render callbacks no longer receive `children` or + `innerHTML`, and returned mixin elements cannot override host subtree content. + `handle.element` and `createElement(handle.element, ...)` are now limited to + patching host props and nested `mix` values. + +- Add an `attrs(...)` mixin for applying default host props through `mix`. + Recipe authors can now provide attributes such as `type="button"` without + overriding explicit JSX props, which makes it easier to build reusable element + recipes that include both styling and safe defaults. + +- Allow nested arrays in `mix` props and flatten them during JSX normalization. + This makes it easier to compose reusable style and behavior recipes without + manually spreading arrays before passing them to elements or components. + +- `renderToStream()` now accepts a `resolveClientEntry(entryId, component)` + callback for resolving opaque client entry IDs during server rendering. + + For example: + + ```ts + import type { RemixNode } from 'remix/ui' + import { renderToStream } from 'remix/ui/server' + + import { resolveEntryId } from './resolve-entry-id.ts' + + export function render(node: RemixNode) { + return renderToStream(node, { + async resolveClientEntry(entryId, component) { + return { + href: await resolveEntryId(entryId), + exportName: entryId.split('#')[1] || component.name, + } + }, + }) + } + ``` + +### Patch Changes + +- Fix controlled `<select>` restore timing so `change` handlers can read and + commit the newly selected value. + + When a browser dispatches `input` before `change` for a select interaction, + Remix Component now defers controlled restoration for selects to the `change` + phase instead of restoring on `input`, which prevents stale controlled values + from clobbering the pending selection before app handlers run. + +- `handle.signal` in mixins now aborts when that specific mixin slot is removed, + even if the host node stays mounted. This fixes cleanup patterns that expect + `handle.signal` to match the mixin `remove` lifecycle. + +## v0.6.0 + +### Minor Changes + +- BREAKING CHANGE: remove legacy host-element `on` prop support in + `@remix-run/ui`. + + Use the `on()` mixin instead: + - Old: `<button on={{ click() {} }} />` + - New: `<button mix={[on('click', () => {})]} />` + + This change removes built-in host `on` handling from runtime, typing, and + host-prop composition. Component-level `handle.on(...)` remains supported. + +- BREAKING CHANGE: remove legacy host-element `css` prop runtime support in + `@remix-run/ui`. + + Use the `css(...)` mixin instead: + - Old: `<div css={{ color: 'red' }} />` + - New: `<div mix={[css({ color: 'red' })]} />` + + This aligns styling behavior with the new mixin composition model. + +- BREAKING CHANGE: remove legacy host-element `animate` prop runtime support in + `@remix-run/ui`. + + Use animation mixins instead: + - Old: `<div animate={{ enter: true, exit: true, layout: true }} />` + - New: `<div mix={[animateEntrance(), animateExit(), animateLayout()]} />` + + This aligns animation behavior with the new mixin composition model. + +- BREAKING CHANGE: remove legacy host-element `connect` prop support in + `@remix-run/ui`. + + Use the `ref(...)` mixin instead: + - Old: `<div connect={(node, signal) => {}} />` + - New: `<div mix={[ref((node, signal) => {})]} />` + + This aligns element reference and teardown behavior with the mixin composition + model. + +- BREAKING CHANGE: the `@remix-run/interaction` package has been removed. + + `handle.on(...)` APIs were also removed from component and mixin handles. + + Before/after migration: + + **Interaction package APIs:** + - Before: `defineInteraction(...)`, `createContainer(...)`, + `on(target, listeners)` from `@remix-run/interaction`. + - After: use component APIs (`createMixin(...)`, `on(...)`, + `addEventListeners(...)`) from `@remix-run/ui`. + + ```ts + // Before + import { on } from '@remix-run/interaction' + + let dispose = on(window, { + resize() { + console.log('resized') + }, + }) + + // After + import { addEventListeners } from '@remix-run/ui' + + let controller = new AbortController() + addEventListeners(window, controller.signal, { + resize() { + console.log('resized') + }, + }) + ``` + + **Component handle API:** + - Before: `handle.on(target, listeners)`. + - After: `addEventListeners(target, handle.signal, listeners)`. + + ```tsx + // Before + function KeyboardTracker(handle: Handle) { + handle.on(document, { + keydown(event) { + console.log(event.key) + }, + }) + return () => null + } + + // After + import { addEventListeners } from '@remix-run/ui' + + function KeyboardTracker(handle: Handle) { + addEventListeners(document, handle.signal, { + keydown(event) { + console.log(event.key) + }, + }) + return () => null + } + ``` + + **Custom interaction patterns:** + - Before: `defineInteraction(...)` + interaction setup function. + - After: event mixins (`createMixin(...)`) that compose `on(...)` listeners + and dispatch typed custom events. + + ```tsx + // Before + import { defineInteraction, type Interaction } from '@remix-run/interaction' + + export let tempo = defineInteraction('my:tempo', Tempo) + + function Tempo(handle: Interaction) { + handle.on(handle.target, { + click() { + handle.target.dispatchEvent(new TempoEvent(bmp)) + }, + }) + } + + // App consumption (before, JSX) + function TempoButtonBefore() { + return () => ( + <button + on={{ + [tempo](event) { + console.log(event.bpm) + }, + }} + /> + ) + } + + // After + import { createMixin, on } from '@remix-run/ui' + + export let tempo = 'my:tempo' as const + + export let tempoEvents = createMixin<HTMLElement>((handle) => { + return () => ( + <handle.element + mix={[ + on('click', (event) => { + event.currentTarget.dispatchEvent(new TempoEvent(bpm)) + }), + ]} + /> + ) + }) + + // App consumption (after) + function TempoButton() { + return () => ( + <button + mix={[ + tempoEvents(), + on(tempo, (event) => { + console.log(event.detail.bpm) + }), + ]} + /> + ) + } + ``` + + **TypedEventTarget** + + `TypedEventTarget` is now exported from `@remix-run/ui`. + +- BREAKING CHANGE: `renderToStream()`, hydration, client updates, and frame + reloads no longer hoist bare `title`, `meta`, `link`, `style`, or + `script[type="application/ld+json"]` elements into `document.head`. Render + head content inside an explicit `<head>` instead, or pass values like `title` + to a layout component that renders the head. + + This removes ordering-sensitive head manipulation from server rendering and + client reconciliation. We originally explored this behavior in the spirit of + React's head "float" work, but Remix Component's async model is centered on + routes and frames rather than async components, so layouts can render head + content explicitly without needing to discover and reorder tags from deep in + the tree. + +- Add the new host `mix` prop and mixin authoring APIs in `@remix-run/ui`. + + New exports include: + - `createMixin` + - `MixinDescriptor`, `MixinHandle`, `MixinType`, `MixValue` + - `on(...)` + - `ref(...)` + - `css(...)` + + This enables reusable host behaviors and composable element capabilities + without bespoke host props. + +- Add new interaction mixins for normalized user input events: + - `pressEvents(...)` for pointer/keyboard "press" interactions + - `keysEvents(...)` for keyboard key state events + + These helpers provide a consistent mixin-based interaction model for input + handling. + +- Add mixin-first animation APIs for host elements: + - `animateEntrance(...)` + - `animateExit(...)` + - `animateLayout(...)` + + These APIs move entrance/exit/layout animation behavior to composable mixins + that can be combined with other host behaviors. + +- Allow the `mix` prop to accept either a single mixin descriptor or an array of + mixin descriptors. + + This lets one-off mixins use `mix={...}` while preserving array support for + composed mixins, and component render props now normalize `mix` to an array or + `undefined` so wrapper components can compose `mix` values without special + casing single descriptors. + +- Allow client `resolveFrame(...)` callbacks to return `RemixNode` content in + addition to HTML strings and streams. + + This lets apps render local frame fallback and recovery UI directly from the + client runtime without manually serializing HTML, and frame updates now clear + previously rendered HTML before mounting the new node-based content. + +- Automatically intercept anchor and area navigations through the Navigation + API, with `rmx-target` to target mounted frames, `rmx-src` to override the + fetched frame source, and `rmx-document` to opt back into full-document + navigation. + +- Add imperative frame-navigation runtime APIs and a + `link(href, { src, target, history })` mixin for declarative client + navigations. + + `run()` now initializes from `run({ loadModule, resolveFrame })`, the package + exports `navigate(href, { src, target, history })` and + `link(href, { src, target, history })`, and components can target mounted + frames via `handle.frames.top` and `handle.frames.get(name)`. The `link()` + mixin adds `href`/`rmx-*` attributes to anchors and gives buttons and other + elements accessible link semantics with click and keyboard navigation + behavior. + +- Allow `resolveFrame(src, signal, target)` to receive the named frame target + for targeted reloads. + + This makes it easier to distinguish targeted frame navigations when forwarding + frame requests through app-specific fetch logic. + +- Add SSR frame source context for nested frame rendering. + + `renderToStream()` now accepts `frameSrc` and `topFrameSrc`, `resolveFrame()` + receives a `ResolveFrameContext`, and server-rendered components can read + stable `handle.frame.src` and `handle.frames.top.src` values across nested + frame renders. + +### Patch Changes + +- Preserve browser-managed live state when frame DOM diffing updates interactive + elements. + + This keeps reloads from clobbering current UI state for reflected and + form-like cases such as `details[open]`, `dialog[open]`, `input.checked`, + editable input values, `textarea` values, `<select>` selection, and open + popovers when the incoming HTML only changes serialized defaults. + +- Forward hydrated client entry, frame reload, and `ready()` initialization + errors to the top-level runtime target returned by `run()`, and type that + runtime as a `TypedEventTarget` with an `error` event whose `.error` value is + `unknown`. + + This lets `app.addEventListener('error', ...)` observe bubbling DOM errors + captured by hydrated client entry roots, frame reload failures such as + rejected `resolveFrame()` calls, and initialization failures that reject + `app.ready()`, while also giving TypeScript-aware consumers the concrete event + names and safer payload types exposed by `run()` and root listeners. + +- Run mixin `insert`, `remove`, and `reclaimed` lifecycle events in the + scheduler's commit phase instead of dispatching them inline during DOM + diffing. + + This lets `ref(...)` and other insert-driven mixins safely call + `handle.update()` during initial mount, and it makes mixin lifecycle timing + line up with commit-phase DOM state before normal queued tasks run. + +- Fix full-document client reloads that could leave orphaned hydration markers + behind when adjacent client entries are diffed in the same parent. + + This prevents later navigations from failing with + `Error: End marker not found` after the live DOM ends up with mismatched + `rmx:h` start and end markers. + +- Fix SVG `className` prop normalization to render as `class` in both client DOM + updates and SSR stream output. + + Also add SVG regression coverage to prevent accidental `class-name` output. + +- Resolve nested SVG click targets back to their enclosing anchor or area + element so frame navigation still intercepts normal link clicks inside inline + SVG content. + +- Skip frame-navigation interception for native anchor and area elements with a + `download` attribute so browsers can handle file downloads normally without + needing `rmx-document`. + +## v0.5.0 + +### Minor Changes + +- BREAKING CHANGE: `handle.update()` now returns `Promise<AbortSignal>` instead + of accepting an optional task callback. + - The promise is resolved when the update is complete (DOM is updated, tasks + have run) + - The signal is aborted when the component updates again or is removed. + + ```tsx + let signal = await handle.update() + // dom is updated + // focus/scroll elements + // do fetches, etc. + ``` + + Note that `await handle.update()` resumes on a microtask after the flush + completes, so the browser may paint before your code runs. For work that must + happen synchronously during the flush (e.g. measuring elements and triggering + another update without flicker), continue to use `handle.queueTask()` instead. + + ```tsx + handle.update() + handle.queueTask(() => { + let rect = widthReferenceNode.getBoundingClientRect() + if (rect.width !== width) { + width = rect.width + handle.update() + } + }) + ``` + +- BREAKING CHANGE: rename virtual root teardown from `remove()` to `dispose()`. + + Old -> new: + - `root.remove()` -> `root.dispose()` (for both `createRoot()` and + `createRangeRoot()` roots) + - `app.remove()` -> `app.dispose()` when using `run(...)` + + This aligns virtual root teardown with `run(...).dispose()` for full-app + cleanup. + +- Add SSR with out-of-order streaming, selective hydration, async frames, and + granular ui refresh + + ADDITIONS: + - `<Frame>` + - `renderToStream(node, { resolveFrame })` + - `clientEntry` + - `run({ loadModule, resolveFrame })` + - `handle.frame` + - `handle.frames` + +### Patch Changes + +- Fix host prop removal to fully remove reflected attributes while still + resetting runtime form control state. + + Adds regression coverage for attribute removal/update behavior to prevent + empty-attribute regressions. + +- Fix updates for nested component-to-element replacements + +- Harden SVG attribute normalization so canonical SVG attribute names are + preserved consistently across server rendering, hydration, and client DOM + updates. + + This fixes rendering/behavior regressions caused by incorrect attribute casing + (including filter and other SVG effect/geometry attributes) and improves + parity with standard React/browser SVG behavior. + +## v0.4.0 + +### Minor Changes + +- Add animation prop, spring, and tween utilities + - `animate` prop on host elements enables enter, exit, and layout (FLIP) + animations + - `spring()` function creates spring-based animation iterators with + configurable stiffness, damping, and mass + - `tween()` function creates time-based animation iterators with customizable + duration and easing (including `easings` presets) + +- `VirtualRoot` now extends `EventTarget` and dispatches `error` events when + errors occur during rendering or in event handlers. Listen for errors via + `root.addEventListener('error', (e) => { ... })`. + +### Patch Changes + +- Change css processing to use data attribute instead of className + +- Add `aspect-ratio` to numeric CSS properties (no longer appends `px` to + numeric values) + +- Bumped `@remix-run/*` dependencies: + - [`@remix-run/interaction@0.5.0`](https://github.com/remix-run/remix/releases/tag/interaction@0.5.0) + +## v0.3.0 + +### Minor Changes + +- BREAKING CHANGE: Updated Component API + - Removed stateless components favoring a single component shape + - Components no longer called with `this` function context + - Introduced `setup` prop + - `setup` prop is passed to the setup function + - `props` are only passed to the render function + + #### Example: + + **Before** + + ```tsx + function Counter( + // `this` binding + this: Handle, + // props available in setup scope + { initialCount }: { initialCount: number }, + ) { + let count = initialCount + + return ({ label }: { label: string }) => ( + <button + on={{ + click: () => { + count++ + this.update() + }, + }} + > + {label} {count} + </button> + ) + } + + let el = <Counter initialCount={10} label="Count" /> + ``` + + **After** + + ```tsx + function Counter( + // handle is a normal parameter + handle: Handle, + // only `setup` prop available in setup scope + setup: number, + ) { + let count = setup + + // props only available in render scope + return (props: { label: string }) => ( + <button + on={{ + click() { + count++ + handle.update() + }, + }} + > + {props.label} {count} + </button> + ) + } + + // usage + let el = <Counter setup={10} label="Count" /> + ``` + + #### Discussion: + + ##### Removing stateless components + + There was conceptual overhead of "stateful vs. stateless components" that is + completely gone. All components must return a render function whether state is + managed or not. + + By having only one component shape, you no longer have to think about when to + return a function and when not to. It also smooths over refactors and the + cognitive overhead of swapping between the two forms as the requirements + change. + + Additionally, the subtle difference between the two forms was hard to spot in + practice. + + ```tsx + // this has a bug + function Counter(this: Handle) { + let count = 0 + return ( + <button + on={{ + click: () => { + count++ + this.update() + }, + }} + > + This has a bug. + </button> + ) + } + + // this was the fix, very hard to spot! + function Counter(this: Handle) { + let count = 0 + return () => ( + <button + on={{ + click: () => { + count++ + this.update() + }, + }} + > + This doesn't + </button> + ) + } + ``` + + The utility of being able to write `return (` instead of `() => (` has little + benefit compared to the risks it created. + - Both `handle` and `props` are optional arguments. + - All components must return a function, there is no longer a distinction + between stateful or stateless components + + ```tsx + // "stateless" component before + function SomeLayout({ children }: { children: RemixNode }) { + return ( + <div> + <h1>Some Title</h1> + <main>{children}</main> + </div> + ) + } + + // after this change (returns a render function) + function SomeLayout() { + return ({ children }: { children: RemixNode }) => ( + <div> + <h1>Some Title</h1> + <main>{children}</main> + </div> + ) + } + ``` + + ##### The `setup` prop + + The `setup` prop exists primarily to keep regular props out of the setup + scope, preventing accidental stale captures. + + When props were available in the setup scope it was easy to accidentally + capture the initial value and then lose updates from parents. + + For example: + + ```tsx + function Counter( + this: Handle, + // captured `label` in the wrong scope + props: { label: string; initialCount: number }, + ) { + let count = initialCount + + return () => ( + <button + on={{ + click: () => { + count++ + this.update() + }, + }} + > + {label /* stale! */} {count} + </button> + ) + } + ``` + + This was particularly troublesome when a component switched from stateless to + stateful. If you forgot to shuffle the props from the setup scope to the newly + created render scope, all of the props are now stale. It was also easy to + define new props for an existing component in the setup scope when it should + have been in the render scope. + + Now it's simply impossible to make these mistakes because the props aren't + available in the setup scope at all. + + ```tsx + function Counter( + handle: Handle, + // only the setup prop is passed here, no access to `label` + setup: { count: number }, + ) { + let count = setup.count + + return ({ label }: { label: string }) => ( + <button + on={{ + click() { + count++ + handle.update() + }, + }} + > + {label} {count} + </button> + ) + } + + let el = <Counter setup={{ count: 10 }} label="Count" /> + ``` + + Now, the only way to make a prop stale is to do it very intentionally: + + ```tsx + // this is a bad example, showing the difficulty and ill-advised method of + // making a prop value static by moving props into the setup scope + function Counter(handle: Handle, setup: number) { + let count = setup + let initialLabel: string + + return (props: { label: string }) => { + // what used to be an accident is now difficult to do on purpose + if (!initialLabel) { + initialLabel = props.label + } + return ( + <button + on={{ + click: () => { + count++ + handle.update() + }, + }} + > + {initialLabel} {count} + </button> + ) + } + } + ``` + + However, it is advised to use the setup prop if you intend for a value to be + static, like `setup.count`. Props that are rendered should typically be props + and not setup. + + ##### `this` binding removal + + We used `this` simply for its "optional first position" characteristic. + Otherwise, it was difficult to decide which parameter should come first: + handle or props? + + ```tsx + // need handle but not props + function PropsFirst(_: PropType, handle: Handle) {} + + // or with a reversed signature, need props but not handle + function HandleFirst(_: Handle, props: PropType) {} + ``` + + Using `this` as an optional context argument solved the problem well: + + ```tsx + function Neither() {} + function Both(this: Handle, props: PropType) {} + function OnlyHandle(this: Handle) {} + function OnlyProps(props: PropType) {} + ``` + + This is no longer a concern since props have been removed from the setup scope + because: + - If you need `setup` then you are likely stateful + - If you are stateful you need the handle + - Therefore `setup` isn't useful without `handle` + + This affords a function signature that doesn't require skipping the first + argument to get access to the second: + + ```tsx + function OnlyHandle(handle: Handle) {} + function Both(handle: Handle, setup: SomeInterface) {} + function Neither() {} + function OnlySetup(_: Handle, setup: SomeInterface) { + // rare: unclear what setup would be used for without a handle + } + ``` + + So without needing `this` for anything other than an optional first argument, + we can remove the constraint. This allows for more flexible function syntax + instead of requiring arrow function expressions everywhere inside a component. + + ```tsx + function Counter(handle: Handle, setup: number) { + let count = setup + + // function declarations inside the setup scope + function updateCount() { + count++ + handle.update() + } + + return (props: { label: string }) => { + return ( + <button + on={{ + // object method shorthand + click() { + updateCount() + }, + }} + > + {props.label} {count} + </button> + ) + } + } + ``` + +### Patch Changes + +- Fix SVG namespace propagation through components + + Components rendered inside `<svg>` elements now correctly create SVG elements + instead of HTML elements. + +- Remove requirement for every element to have props + + Originally, `@remix/ui` assumed that props will be an object, not `null` or + `undefined`. This requirement has been removed and allows props to be nullish. + This makes it easier to render `@remix/ui` apps using alternative JSX + templating tools like [`htm`](https://www.npmjs.com/package/htm). + +## v0.2.1 (2025-12-19) + +- Fix node replacement + + Anchors were being calculated incorrectly because it removed the old node + before inserting the new one, Now it correctly uses the old node as the anchor + for insertion and inserts the new node before removing the old one. + +## v0.2.0 (2025-12-18) + +- This is the initial release of the component package. + + See the + [README](https://github.com/remix-run/remix/blob/main/packages/ui/README.md) + for more information. + +## Unreleased + +- Initial release diff --git a/docs/agents/remix/ui/docs/component.md b/docs/agents/remix/ui/docs/component.md new file mode 100644 index 0000000..338835f --- /dev/null +++ b/docs/agents/remix/ui/docs/component.md @@ -0,0 +1,786 @@ +# component + +A minimal component system built on JavaScript and DOM primitives. Write +components that render on the server, stream to the browser, and hydrate only +where you need interactivity. + +## Features + +- **JSX Runtime** - Convenient JSX syntax +- **Component State** - State managed with plain JavaScript variables +- **Manual Updates** - Explicit control over when components update via + `handle.update()` +- **Real DOM Events** - Events are real DOM events using the `on()` mixin and + `addEventListeners()` +- **Inline CSS** - `css(...)` mixin with pseudo-selectors and nested rules +- **Server Rendering** - Stream full pages or fragments with `renderToStream` +- **Hydration** - Mark interactive components with `clientEntry` and hydrate + them on the client with `run` +- **Frames** - `<Frame>` streams partial server UI into the page and can be + reloaded without a full page navigation + +## Installation + +```sh +npm i remix +``` + +## Quick Start + +### Server + +Render a full page to a streaming response: + +```tsx +import { renderToStream } from 'remix/ui/server' +import { Frame } from 'remix/ui' +import { Counter } from './assets/counter.tsx' + +function App() { + return () => ( + <html> + <head> + <title>My App + ' -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 = '
    Title
    ' -let body = '
    Content
    ' -let footer = '
    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/file-storage-s3/index.md b/docs/agents/remix/file-storage-s3/index.md deleted file mode 100644 index ef7c020..0000000 --- a/docs/agents/remix/file-storage-s3/index.md +++ /dev/null @@ -1,57 +0,0 @@ - - -# 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/index.md b/docs/agents/remix/file-storage/index.md deleted file mode 100644 index f182d70..0000000 --- a/docs/agents/remix/file-storage/index.md +++ /dev/null @@ -1,64 +0,0 @@ - - -# file-storage - -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) -- **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 - -```sh -npm i remix -``` - -## Usage - -### File System - -```ts -import { createFsFileStorage } from 'remix/file-storage/fs' - -let storage = createFsFileStorage('./user/files') - -let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' }) -let key = 'hello-key' - -// Put the file in storage. -await storage.set(key, file) - -// Then, sometime later... -let fileFromStorage = await storage.get(key) -// All of the original file's metadata is intact -fileFromStorage.name // 'hello.txt' -fileFromStorage.type // 'text/plain' - -// To remove from storage -await storage.remove(key) -``` - -## Related Packages - -- [`file-storage-s3`](https://github.com/remix-run/remix/tree/main/packages/file-storage-s3) - - S3 backend for `file-storage` -- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - - Pairs well with this library for storing `FileUpload` objects received in - `multipart/form-data` requests -- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - - The streaming `File` implementation used internally to stream files from - storage - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/form-data-middleware/index.md b/docs/agents/remix/form-data-middleware/index.md deleted file mode 100644 index ccd17fc..0000000 --- a/docs/agents/remix/form-data-middleware/index.md +++ /dev/null @@ -1,123 +0,0 @@ - - -# 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/index.md b/docs/agents/remix/form-data-parser/index.md deleted file mode 100644 index 6ff9428..0000000 --- a/docs/agents/remix/form-data-parser/index.md +++ /dev/null @@ -1,201 +0,0 @@ - - -# form-data-parser - -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 -streaming directly to disk or cloud storage services like -[AWS S3](https://aws.amazon.com/s3/) or -[Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/), preventing -server crashes from memory exhaustion. - -## Features - -- **Drop-in replacement** for `request.formData()` with streaming file upload - support -- **Minimal buffering** - processes file upload streams with minimal memory - footprint -- **Standards-based** - built on the - [web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) - and [File API](https://developer.mozilla.org/en-US/docs/Web/API/File) -- **Smart fallback** - automatically uses native `request.formData()` for - non-`multipart/form-data` requests -- **Storage agnostic** - works with any storage backend (local disk, S3, R2, - etc.) - -## Why You Need This - -The native -[`request.formData()` method](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData) -has a few major flaws in server environments: - -- It buffers all file uploads in memory -- It does not provide fine-grained control over file upload handling -- It does not prevent DoS attacks from malicious requests - -In normal usage, this makes it difficult to process requests with large file -uploads because they can exhaust your server's RAM and crash the application. - -For attackers, this creates an attack vector where malicious actors can -overwhelm your server's memory by sending large payloads with many files. - -`form-data-parser` solves this by handling file uploads as they arrive in the -request body stream, allowing you to safely store files and use either a) the -`File` directly or b) a unique identifier for that file in the returned -`FormData` object. - -## Installation - -```sh -npm i remix -``` - -## Usage - -The `parseFormData` interface allows you to define an "upload handler" function -for fine-grained control of handling file uploads. - -```ts -import * as fsp from 'node:fs/promises' -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) { - // Is this file upload from the field? - if (fileUpload.fieldName === 'user-avatar') { - let filename = `/uploads/user-${user.id}-avatar.bin` - - // Store the file safely on disk - await fsp.writeFile(filename, fileUpload.bytes) - - // Return the file name to use in the FormData object so we don't - // keep the file contents around in memory. - return filename - } - - // Ignore unrecognized fields -} - -// Handle form submissions with file uploads -async function requestHandler(request: Request) { - // Parse the form data from the request.body stream, passing any files - // through your upload handler as they are parsed from the stream - let formData = await parseFormData(request, uploadHandler) - - let avatarFilename = formData.get('user-avatar') - - if (avatarFilename != null) { - console.log(`User avatar uploaded to ${avatarFilename}`) - } else { - console.log(`No user avatar file was uploaded`) - } -} -``` - -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`. - -Known limit errors are thrown directly so you can handle them with `instanceof` -checks. Other failures while parsing the request body are wrapped in -`FormDataParseError`, with the original error available as `error.cause`. Errors -thrown or rejected by your `uploadHandler` are not wrapped. - -```ts -import { - FormDataParseError, - MaxFilesExceededError, - MaxFileSizeExceededError, - MaxHeaderSizeExceededError, - MaxPartsExceededError, - MaxTotalSizeExceededError, -} from 'remix/form-data-parser' - -const oneKb = 1024 -const oneMb = 1024 * oneKb - -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 if (error instanceof FormDataParseError) { - console.error(`Could not parse form data:`, error.cause ?? error) - } else { - throw error - } -} -``` - -If you're looking for a more flexible storage solution for `File` objects that -are uploaded, this library pairs really well with -[the `file-storage` library](https://github.com/remix-run/remix/tree/main/packages/file-storage) -for keeping files in various storage backends. - -```ts -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') - -// Define how to handle incoming file uploads -async function uploadHandler(fileUpload: FileUpload) { - // Is this file upload from the field? - if (fileUpload.fieldName === 'user-avatar') { - let storageKey = `user-${user.id}-avatar` - - // Put the file in storage - await fileStorage.set(storageKey, fileUpload) - - // Return a lazy File object that can access the stored file when needed - return fileStorage.get(storageKey) - } - - // Ignore unrecognized fields -} -``` - -## Demos - -The -[`demos` directory](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos) -contains working demos: - -- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos/node) - - using form-data-parser with file-storage in Node.js - -## 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 -- [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) - - The parser used internally for parsing `multipart/form-data` HTTP messages - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/fs/index.md b/docs/agents/remix/fs/index.md deleted file mode 100644 index ea1e409..0000000 --- a/docs/agents/remix/fs/index.md +++ /dev/null @@ -1,73 +0,0 @@ - - -# fs - -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. - -## Features - -- **Web Standards** - Uses - [`LazyFile`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - which matches the native - [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) API and - provides `.stream()`, `.toFile()`, and `.toBlob()` for converting to native - types. -- **Seamless Node.js Compat** - Works seamlessly with Node.js file descriptors - and handles - -## Installation - -```sh -npm i remix -``` - -## Usage - -### Opening Lazy Files - -```ts -import { openLazyFile } from 'remix/fs' - -// Open a file from the filesystem -let lazyFile = openLazyFile('./path/to/file.json') - -// The file is lazy - no data is read until you call lazyFile.text(), lazyFile.bytes(), etc. -let json = JSON.parse(await lazyFile.text()) - -// You can override file metadata -let customLazyFile = openLazyFile('./image.jpg', { - name: 'custom-name.jpg', - type: 'image/jpeg', - lastModified: Date.now(), -}) -``` - -### Writing Files - -```ts -import { openLazyFile, writeFile } from 'remix/fs' - -// Read a file and write it elsewhere -let lazyFile = openLazyFile('./source.txt') -await writeFile('./destination.txt', lazyFile) - -// Write to an open file handle -import * as fsp from 'node:fs/promises' -let handle = await fsp.open('./destination.txt', 'w') -await writeFile(handle, lazyFile) -await handle.close() -``` - -## Related Packages - -- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - - Lazy, streaming `Blob`/`File` implementation -- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - - Storage abstraction for files - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/headers/index.md b/docs/agents/remix/headers/index.md deleted file mode 100644 index e508cc0..0000000 --- a/docs/agents/remix/headers/index.md +++ /dev/null @@ -1,620 +0,0 @@ - - -# 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](./index.md#accept) -- [Accept-Encoding](./index.md#accept-encoding) -- [Accept-Language](./index.md#accept-language) -- [Cache-Control](./index.md#cache-control) -- [Content-Disposition](./index.md#content-disposition) -- [Content-Range](./index.md#content-range) -- [Content-Type](./index.md#content-type) -- [Cookie](./index.md#cookie) -- [If-Match](./index.md#if-match) -- [If-None-Match](./index.md#if-none-match) -- [If-Range](./index.md#if-range) -- [Range](./index.md#range) -- [Set-Cookie](./index.md#set-cookie) -- [Vary](./index.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/html-template/index.md b/docs/agents/remix/html-template/index.md deleted file mode 100644 index 8108b41..0000000 --- a/docs/agents/remix/html-template/index.md +++ /dev/null @@ -1,112 +0,0 @@ - - -# html-template - -Safe HTML template literals for Remix. `html-template` automatically escapes -interpolated values to prevent XSS while still supporting explicit trusted HTML -insertion. - -## Features - -- **Automatic HTML escaping** - All interpolated values are escaped by default -- **Explicit raw HTML** - Use `html.raw` when you need unescaped HTML from - trusted sources -- **Composable** - SafeHtml values can be nested without double-escaping -- **Type-safe** - Full TypeScript support with branded types -- **Zero dependencies** - Lightweight and self-contained -- **Runtime agnostic** - Works in Node.js, Bun, Deno, browsers, and edge - runtimes - -## Installation - -```sh -npm i remix -``` - -## Usage - -```ts -import { html } from 'remix/html-template' - -let userInput = '' -let greeting = html`

    Hello ${userInput}!

    ` - -console.log(String(greeting)) -// Output:

    Hello <script>alert("XSS")</script>!

    -``` - -By default, all interpolated values are automatically escaped to prevent XSS -attacks. - -If you have trusted HTML that should not be escaped, use `html.raw`: - -```ts -import { html } from 'remix/html-template' - -let trustedIcon = '...' -let button = html.raw`` - -console.log(String(button)) -// => -``` - -**Warning**: Only use `html.raw` with content you trust. Never use it with user -input. - -### Composing HTML Fragments - -SafeHtml values can be nested without double-escaping: - -```ts -import { html } from 'remix/html-template' - -let title = html`

    My Title

    ` -let content = html`

    Some content with ${userInput}

    ` - -let page = html` - - - - ${title} ${content} - - -` -``` - -### Working with Arrays - -You can interpolate arrays of values, which will be flattened and joined: - -```ts -import { html } from 'remix/html-template' - -let items = ['Apple', 'Banana', 'Cherry'] -let list = html` -
      - ${items.map((item) => html`
    • ${item}
    • `)} -
    -` -``` - -### Conditional Rendering - -Use `null` or `undefined` to render nothing: - -```ts -import { html } from 'remix/html-template' - -let showError = false -let errorMessage = 'Something went wrong' -let page = html`
    - ${showError ? html`
    ${errorMessage}
    ` : null} -
    ` -``` - -## Related Packages - -- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - HTTP router that works great with html-template - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/index.md b/docs/agents/remix/index.md deleted file mode 100644 index 88970b6..0000000 --- a/docs/agents/remix/index.md +++ /dev/null @@ -1,178 +0,0 @@ -# Remix packages - -Downloaded from -https://github.com/remix-run/remix/tree/remix@3.0.0-beta.0/packages. - -Use this index to find downloaded package docs for Remix v3 beta.0. - -## Start here - -- [remix](./remix/index.md) - The Remix web framework -- [cli](./cli/index.md) - Command-line interface for Remix -- [ui](./ui/index.md) - UI tokens, mixins, and glyphs for Remix components (16 - nested docs) -- [fetch-router](./fetch-router/index.md) - A minimal, composable router for the - web Fetch API -- [route-pattern](./route-pattern/index.md) - Match and generate URLs with - strong typing -- [node-fetch-server](./node-fetch-server/index.md) - Build servers for Node.js - using the web fetch API -- [node-serve](./node-serve/index.md) - Build high-performance Fetch API servers - for Node.js -- [test](./test/index.md) - A test framework for JavaScript and TypeScript - projects - -## UI and assets - -- [ui](./ui/index.md) - UI tokens, mixins, and glyphs for Remix components (16 - nested docs) -- [assets](./assets/index.md) - Fetch-based server for compiling browser JS/TS - and CSS assets on demand -- [html-template](./html-template/index.md) - HTML template tag with - auto-escaping for JavaScript - -## Routing, requests, and middleware - -- [fetch-router](./fetch-router/index.md) - A minimal, composable router for the - web Fetch API -- [route-pattern](./route-pattern/index.md) - Match and generate URLs with - strong typing -- [node-fetch-server](./node-fetch-server/index.md) - Build servers for Node.js - using the web fetch API -- [node-serve](./node-serve/index.md) - Build high-performance Fetch API servers - for Node.js -- [fetch-proxy](./fetch-proxy/index.md) - An HTTP proxy for the web Fetch API -- [async-context-middleware](./async-context-middleware/index.md) - Middleware - for storing request context in AsyncLocalStorage -- [auth-middleware](./auth-middleware/index.md) - Pluggable authentication - middleware for Remix -- [compression-middleware](./compression-middleware/index.md) - Middleware for - compressing HTTP responses -- [cop-middleware](./cop-middleware/index.md) - Middleware for tokenless - cross-origin protection in Fetch API servers -- [cors-middleware](./cors-middleware/index.md) - Middleware for handling CORS - in Fetch API servers -- [csrf-middleware](./csrf-middleware/index.md) - Middleware for CSRF protection - in Fetch API servers -- [logger-middleware](./logger-middleware/index.md) - Middleware for logging - HTTP requests and responses -- [method-override-middleware](./method-override-middleware/index.md) - - Middleware for overriding HTTP request methods from form data -- [static-middleware](./static-middleware/index.md) - Middleware for serving - static files from the filesystem - -## Auth, sessions, and cookies - -- [auth](./auth/index.md) - Browser login, OAuth, and OIDC helpers for Remix -- [auth-middleware](./auth-middleware/index.md) - Pluggable authentication - middleware for Remix -- [session](./session/index.md) - Session management for JavaScript -- [session-middleware](./session-middleware/index.md) - Middleware for managing - sessions with cookie-based storage -- [session-storage-memcache](./session-storage-memcache/index.md) - Memcache - session storage for remix/session -- [session-storage-redis](./session-storage-redis/index.md) - Redis session - storage for remix/session -- [cookie](./cookie/index.md) - A toolkit for working with cookies in JavaScript -- [csrf-middleware](./csrf-middleware/index.md) - Middleware for CSRF protection - in Fetch API servers - -## Data and storage - -- [data-schema](./data-schema/index.md) - Tiny, standards-aligned schema - validation -- [data-table](./data-table/index.md) - A typed, relational query toolkit for - JavaScript -- [data-table-mysql](./data-table-mysql/index.md) - MySQL adapter for - remix/data-table -- [data-table-postgres](./data-table-postgres/index.md) - PostgreSQL adapter for - remix/data-table -- [data-table-sqlite](./data-table-sqlite/index.md) - SQLite adapter for - remix/data-table -- [file-storage](./file-storage/index.md) - Key/value storage for JavaScript - File objects -- [file-storage-s3](./file-storage-s3/index.md) - S3 backend for - remix/file-storage -- [fs](./fs/index.md) - Filesystem utilities using the Web File API -- [lazy-file](./lazy-file/index.md) - Lazy, streaming files for JavaScript - -## Responses, headers, uploads, and parsing - -- [response](./response/index.md) - Response helpers for the web Fetch API -- [headers](./headers/index.md) - A toolkit for working with HTTP headers in - JavaScript -- [form-data-middleware](./form-data-middleware/index.md) - Middleware for - parsing FormData from request bodies -- [form-data-parser](./form-data-parser/index.md) - A request.formData() wrapper - with streaming file upload handling -- [multipart-parser](./multipart-parser/index.md) - A fast, efficient parser for - multipart streams in any JavaScript environment -- [mime](./mime/index.md) - Utilities for working with MIME types -- [tar-parser](./tar-parser/index.md) - A fast, efficient parser for tar streams - in any JavaScript environment - -## Testing and terminal utilities - -- [test](./test/index.md) - A test framework for JavaScript and TypeScript - projects -- [terminal](./terminal/index.md) - Terminal output utilities for JavaScript - libraries and CLIs -- [assert](./assert/index.md) - Node assert-compatible utilities for any - JavaScript environment - -## Package map - -| Package | Version | Focus | Docs | -| -------------------------- | ------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| assert | 0.2.0 | Node assert-compatible utilities for any JavaScript environment | [assert](./assert/index.md) | -| assets | 0.3.0 | Fetch-based server for compiling browser JS/TS and CSS assets on demand | [assets](./assets/index.md) | -| async-context-middleware | 0.2.2 | Middleware for storing request context in AsyncLocalStorage | [async-context-middleware](./async-context-middleware/index.md) | -| auth | 0.2.1 | Browser login, OAuth, and OIDC helpers for Remix | [auth](./auth/index.md) | -| auth-middleware | 0.1.2 | Pluggable authentication middleware for Remix | [auth-middleware](./auth-middleware/index.md) | -| cli | 0.2.0 | Command-line interface for Remix | [cli](./cli/index.md) | -| compression-middleware | 0.1.7 | Middleware for compressing HTTP responses | [compression-middleware](./compression-middleware/index.md) | -| cookie | 0.5.1 | A toolkit for working with cookies in JavaScript | [cookie](./cookie/index.md) | -| cop-middleware | 0.1.2 | Middleware for tokenless cross-origin protection in Fetch API servers | [cop-middleware](./cop-middleware/index.md) | -| cors-middleware | 0.1.2 | Middleware for handling CORS in Fetch API servers | [cors-middleware](./cors-middleware/index.md) | -| csrf-middleware | 0.1.2 | Middleware for CSRF protection in Fetch API servers | [csrf-middleware](./csrf-middleware/index.md) | -| data-schema | 0.3.0 | Tiny, standards-aligned schema validation | [data-schema](./data-schema/index.md) | -| data-table | 0.2.1 | A typed, relational query toolkit for JavaScript | [data-table](./data-table/index.md) | -| data-table-mysql | 0.3.1 | MySQL adapter for remix/data-table | [data-table-mysql](./data-table-mysql/index.md) | -| data-table-postgres | 0.3.1 | PostgreSQL adapter for remix/data-table | [data-table-postgres](./data-table-postgres/index.md) | -| data-table-sqlite | 0.4.1 | SQLite adapter for remix/data-table | [data-table-sqlite](./data-table-sqlite/index.md) | -| fetch-proxy | 0.8.0 | An HTTP proxy for the web Fetch API | [fetch-proxy](./fetch-proxy/index.md) | -| fetch-router | 0.18.2 | A minimal, composable router for the web Fetch API | [fetch-router](./fetch-router/index.md) | -| file-storage | 0.13.4 | Key/value storage for JavaScript File objects | [file-storage](./file-storage/index.md) | -| file-storage-s3 | 0.1.1 | S3 backend for remix/file-storage | [file-storage-s3](./file-storage-s3/index.md) | -| form-data-middleware | 0.2.3 | Middleware for parsing FormData from request bodies | [form-data-middleware](./form-data-middleware/index.md) | -| form-data-parser | 0.17.0 | A request.formData() wrapper with streaming file upload handling | [form-data-parser](./form-data-parser/index.md) | -| fs | 0.4.3 | Filesystem utilities using the Web File API | [fs](./fs/index.md) | -| headers | 0.19.0 | A toolkit for working with HTTP headers in JavaScript | [headers](./headers/index.md) | -| html-template | 0.3.0 | HTML template tag with auto-escaping for JavaScript | [html-template](./html-template/index.md) | -| lazy-file | 5.0.3 | Lazy, streaming files for JavaScript | [lazy-file](./lazy-file/index.md) | -| logger-middleware | 0.2.1 | Middleware for logging HTTP requests and responses | [logger-middleware](./logger-middleware/index.md) | -| method-override-middleware | 0.1.7 | Middleware for overriding HTTP request methods from form data | [method-override-middleware](./method-override-middleware/index.md) | -| mime | 0.4.1 | Utilities for working with MIME types | [mime](./mime/index.md) | -| multipart-parser | 0.16.0 | A fast, efficient parser for multipart streams in any JavaScript environment | [multipart-parser](./multipart-parser/index.md) | -| node-fetch-server | 0.13.1 | Build servers for Node.js using the web fetch API | [node-fetch-server](./node-fetch-server/index.md) | -| node-serve | 0.1.0 | Build high-performance Fetch API servers for Node.js | [node-serve](./node-serve/index.md) | -| remix | 3.0.0-beta.0 | The Remix web framework | [remix](./remix/index.md) | -| response | 0.3.3 | Response helpers for the web Fetch API | [response](./response/index.md) | -| route-pattern | 0.20.1 | Match and generate URLs with strong typing | [route-pattern](./route-pattern/index.md) | -| session | 0.4.1 | Session management for JavaScript | [session](./session/index.md) | -| session-middleware | 0.2.2 | Middleware for managing sessions with cookie-based storage | [session-middleware](./session-middleware/index.md) | -| session-storage-memcache | 0.1.0 | Memcache session storage for remix/session | [session-storage-memcache](./session-storage-memcache/index.md) | -| session-storage-redis | 0.1.0 | Redis session storage for remix/session | [session-storage-redis](./session-storage-redis/index.md) | -| static-middleware | 0.4.8 | Middleware for serving static files from the filesystem | [static-middleware](./static-middleware/index.md) | -| tar-parser | 0.7.1 | A fast, efficient parser for tar streams in any JavaScript environment | [tar-parser](./tar-parser/index.md) | -| terminal | 0.1.0 | Terminal output utilities for JavaScript libraries and CLIs | [terminal](./terminal/index.md) | -| test | 0.3.0 | A test framework for JavaScript and TypeScript projects | [test](./test/index.md) | -| ui | 0.1.1 | UI tokens, mixins, and glyphs for Remix components | [ui](./ui/index.md) + [16 docs](./ui/docs/) | - -## Update instructions - -1. Delete `docs/agents/remix` and redownload package `README.md` and - `docs/**/*.md` from the matching Remix tag. -2. Keep only package source documentation files. -3. Regenerate this index, then run docs formatting, inventory, content, and - local-link checks. diff --git a/docs/agents/remix/lazy-file/index.md b/docs/agents/remix/lazy-file/index.md deleted file mode 100644 index 78036f6..0000000 --- a/docs/agents/remix/lazy-file/index.md +++ /dev/null @@ -1,138 +0,0 @@ - - -# lazy-file - -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 -[File](https://developer.mozilla.org/en-US/docs/Web/API/File)-like objects that -defer reading their contents until needed, which is ideal for situations where a -file's contents do not fit in memory all at once. When file contents are read, -they are streamed to avoid buffering. - -## Features - -- **Deferred Loading** - Blob/file contents loaded on demand to minimize memory - usage -- **Familiar Interface** - `LazyBlob` and `LazyFile` implement the same - interface as native `Blob` and `File` -- **Easy Conversion** - Convert to native `ReadableStream` with `.stream()`, or - to native `Blob`/`File` with `.toBlob()` and `.toFile()` -- **Standard Constructors** - Accepts all the same content types as the original - [`Blob()`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob) and - [`File()`](https://developer.mozilla.org/en-US/docs/Web/API/File/File) - constructors -- **Slice Support** - Supports - [`Blob.slice()`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice), - even on streaming content - -## 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 -don't want to buffer file contents. In particular, -[`the File() constructor`](https://developer.mozilla.org/en-US/docs/Web/API/File/File) -requires the contents of a file to be supplied up front when the object is first -created, like this: - -```ts -let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' }) -``` - -A `LazyFile` improves this model by accepting an additional content type in its -constructor: `LazyContent`. - -```ts -let lazyContent: LazyContent = { - /* See below for usage */ -} -let lazyFile = new LazyFile(lazyContent, 'hello.txt', { type: 'text/plain' }) -``` - -All other `File` functionality works as you'd expect. - -## Installation - -```sh -npm i remix -``` - -## Usage - -The low-level API can be used to create a `LazyFile` that streams content from -anywhere: - -```ts -import { type LazyContent, LazyFile } from 'remix/lazy-file' - -let content: LazyContent = { - // The total length of this file in bytes. - byteLength: 100000, - // A function that provides a stream of data for the file contents, - // beginning at the `start` index and ending at `end`. - stream(start, end) { - // ... read the file contents from somewhere and return a ReadableStream - return new ReadableStream({ - start(controller) { - controller.enqueue('X'.repeat(100000).slice(start, end)) - controller.close() - }, - }) - }, -} - -let lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' }) -await lazyFile.arrayBuffer() // ArrayBuffer of the file's content -lazyFile.name // "example.txt" -lazyFile.type // "text/plain" -``` - -All file contents are read on-demand and nothing is ever buffered unless you -explicitly call `.toFile()` or `.toBlob()`. - -### Streaming Content - -Use `.stream()` to get a `ReadableStream` for `Response` and other streaming -APIs: - -```ts -import { openLazyFile } from 'remix/fs' - -let lazyFile = openLazyFile('./large-video.mp4') - -let response = new Response(lazyFile.stream(), { - headers: { - 'Content-Type': lazyFile.type, - 'Content-Length': String(lazyFile.size), - }, -}) -``` - -### Converting to Native File/Blob - -For non-streaming APIs that require a complete `File` or `Blob` (e.g. -`FormData`), use `.toFile()` or `.toBlob()`. - -```ts -let lazyFile = openLazyFile('./document.pdf') -let realFile = await lazyFile.toFile() - -let formData = new FormData() -formData.append('document', realFile) -``` - -> **Note:** `.toFile()` and `.toBlob()` read the entire file into memory. Only -> use these for non-streaming APIs that require a complete `File` or `Blob` -> (e.g. `FormData`). Always prefer `.stream()` if possible. - -## Related Packages - -- [`fs`](https://github.com/remix-run/remix/tree/main/packages/fs) - Filesystem - utilities for reading and writing files using the Web `File` API -- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - - Storage abstraction for files on disk or in memory - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/logger-middleware/index.md b/docs/agents/remix/logger-middleware/index.md deleted file mode 100644 index 503b249..0000000 --- a/docs/agents/remix/logger-middleware/index.md +++ /dev/null @@ -1,136 +0,0 @@ - - -# logger-middleware - -HTTP request/response logging middleware for Remix. It logs request metadata and -response details with configurable output formats. - -## Features - -- **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 -- **Colorized Output** - Highlights method, status, duration, and content length - in TTY output - -## Installation - -```sh -npm i remix -``` - -## Usage - -```ts -import { createRouter } from 'remix/fetch-router' -import { logger } from 'remix/logger-middleware' - -let router = createRouter({ - middleware: [logger()], -}) - -// Logs: [19/Nov/2025:14:32:10 -0800] GET /users/123 200 1234 -``` - -### Custom Format - -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) -- `%dateISO` - Date and time in ISO format -- `%duration` - Request duration in milliseconds -- `%contentLength` - Response Content-Length header -- `%contentType` - Response Content-Type header -- `%host` - Request URL host -- `%hostname` - Request URL hostname -- `%method` - Request method -- `%path` - Request pathname + search -- `%pathname` - Request pathname -- `%port` - Request port -- `%query` - Request query string (search) -- `%referer` - Request Referer header -- `%search` - Request search string -- `%status` - Response status code -- `%statusText` - Response status text -- `%url` - Full request URL -- `%userAgent` - Request User-Agent header - -```ts -let router = createRouter({ - middleware: [ - logger({ - format: '%method %path - %status (%duration ms)', - }), - ], -}) -// Logs: GET /users/123 - 200 (42 ms) -``` - -For Apache-style combined log format, you can use the following format: - -```ts -let router = createRouter({ - middleware: [ - logger({ - format: - '%host - - [%date] "%method %path" %status %contentLength "%referer" "%userAgent"', - }), - ], -}) -``` - -### Colorized Output - -Logger output automatically uses ANSI colors for high-signal tokens when -terminal color detection allows them. Set `colors` to `false` to disable -colorized output or `true` to force it on. When the `process` global is defined, -color detection respects `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, and TTY -output streams. - -```ts -let router = createRouter({ - middleware: [ - logger({ - colors: false, - }), - ], -}) -``` - -The following tokens are colorized when colors are enabled: - -- `%method` -- `%status` -- `%duration` -- `%contentLength` - -### Custom Logger - -You can use a custom logger to write logs to a file or other stream. - -```ts -import { createWriteStream } from 'node:fs' - -let logStream = createWriteStream('access.log', { flags: 'a' }) - -let router = createRouter({ - middleware: [ - logger({ - log(message) { - logStream.write(message + '\n') - }, - }), - ], -}) -``` - -## Related Packages - -- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - Router for the web Fetch API - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/method-override-middleware/index.md b/docs/agents/remix/method-override-middleware/index.md deleted file mode 100644 index 46c1b41..0000000 --- a/docs/agents/remix/method-override-middleware/index.md +++ /dev/null @@ -1,82 +0,0 @@ - - -# method-override-middleware - -Method override middleware for Remix. It allows HTML forms to simulate `PUT`, -`PATCH`, and `DELETE` requests using a hidden form field. - -## Features - -- **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 -npm i remix -``` - -## Usage - -This middleware runs after -[the `formData` middleware](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) -and updates the request context's `context.method` with the value of the method -override field. This is useful for simulating RESTful API request methods like -PUT and DELETE using HTML forms. - -```ts -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 - middleware: [formData(), methodOverride()], -}) - -router.delete('/users/:id', async (context) => { - let userId = context.params.id - // Delete user logic... - return new Response('User deleted') -}) -``` - -In your HTML form: - -```html -
    - - - -``` - -### Custom Field Name - -You can customize the name of the method override field by passing a `fieldName` -option to the `methodOverride()` middleware. - -```ts -let router = createRouter({ - middleware: [formData(), methodOverride({ fieldName: '__method__' })], -}) -``` - -```html -
    - - - -``` - -## Related Packages - -- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - Router for the web Fetch API -- [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - - Required for parsing form data - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/mime/index.md b/docs/agents/remix/mime/index.md deleted file mode 100644 index 0a047cd..0000000 --- a/docs/agents/remix/mime/index.md +++ /dev/null @@ -1,120 +0,0 @@ - - -# 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. - -## Features - -- **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 - -```sh -npm i remix -``` - -## Usage - -### `detectMimeType(extension)` - -Detects the MIME type for a given file extension or filename. - -```ts -import { detectMimeType } from 'remix/mime' - -detectMimeType('txt') // 'text/plain' -detectMimeType('.txt') // 'text/plain' -detectMimeType('file.txt') // 'text/plain' -detectMimeType('path/to/file.txt') // 'text/plain' -detectMimeType('unknown') // undefined -``` - -### `detectContentType(extension)` - -Detects the Content-Type header value for a given file extension or filename, -including `charset` for text-based types. See -[`mimeTypeToContentType`](#mimetypetocontenttypemimetype) for charset logic. - -```ts -import { detectContentType } from 'remix/mime' - -detectContentType('css') // 'text/css; charset=utf-8' -detectContentType('.json') // 'application/json; charset=utf-8' -detectContentType('image.png') // 'image/png' -detectContentType('path/to/file.unknown') // undefined -``` - -### `isCompressibleMimeType(mimeType)` - -Checks if a MIME type is known to be compressible. - -```ts -import { isCompressibleMimeType } from 'remix/mime' - -isCompressibleMimeType('text/html') // true -isCompressibleMimeType('application/json') // true -isCompressibleMimeType('image/png') // false -isCompressibleMimeType('video/mp4') // false -``` - -For convenience, the function also accepts a full Content-Type header value: - -```ts -import { isCompressibleMimeType } from 'remix/mime' - -isCompressibleMimeType('text/html; charset=utf-8') // true -isCompressibleMimeType('application/json; charset=utf-8') // true -isCompressibleMimeType('image/png; charset=utf-8') // false -isCompressibleMimeType('video/mp4; charset=utf-8') // false -``` - -### `mimeTypeToContentType(mimeType)` - -Converts a MIME type to a Content-Type header value, adding `; charset=utf-8` to -text-based MIME types: `text/*` (except `text/xml` which has built-in encoding -declarations), `application/json`, `application/javascript`, and all `+json` -suffixed types. All other types are returned unchanged. - -```ts -import { mimeTypeToContentType } from 'remix/mime' - -mimeTypeToContentType('text/css') // 'text/css; charset=utf-8' -mimeTypeToContentType('application/json') // 'application/json; charset=utf-8' -mimeTypeToContentType('application/ld+json') // 'application/ld+json; charset=utf-8' -mimeTypeToContentType('image/png') // 'image/png' -``` - -### `defineMimeType(definition)` - -Registers or overrides a MIME type for one or more file extensions. - -```ts -import { defineMimeType } from 'remix/mime' - -defineMimeType({ - extensions: ['myformat'], - mimeType: 'application/x-myformat', -}) -``` - -You can also optionally configure the charset and whether the MIME type is -compressible: - -```ts -defineMimeType({ - extensions: ['myformat'], - mimeType: 'application/x-myformat', - compressible: true, - charset: 'utf-8', -}) -``` - -## License - -MIT diff --git a/docs/agents/remix/multipart-parser/index.md b/docs/agents/remix/multipart-parser/index.md deleted file mode 100644 index 772ff89..0000000 --- a/docs/agents/remix/multipart-parser/index.md +++ /dev/null @@ -1,298 +0,0 @@ - - -# 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}`) - console.log(`Content-Type header: ${part.headers['content-type']}`) - - // 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) - } - } -} -``` - -## Part Headers - -Each `MultipartPart` exposes decoded part headers as a plain object keyed by -lower-case header name. Values are strings, and repeated headers are joined with -`, `. Multipart part headers are parsed metadata from the request body, not -native `Headers` objects, so access them with bracket notation: - -```ts -for await (let part of parseMultipartRequest(request)) { - let contentDisposition = part.headers['content-disposition'] - let contentType = part.headers['content-type'] - - console.log(contentDisposition, contentType) -} -``` - -## 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 `Content-Disposition` and `Content-Type` metadata 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/node-fetch-server/index.md b/docs/agents/remix/node-fetch-server/index.md deleted file mode 100644 index bfedb0b..0000000 --- a/docs/agents/remix/node-fetch-server/index.md +++ /dev/null @@ -1,379 +0,0 @@ - - -# 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 -- **Node.js HTTP Integration** - Works directly with `node:http`, `node:https`, - and `node:http2` -- **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 -``` - -## Usage - -Use `createRequestListener()` when you want to plug a fetch handler into a -standard Node.js server: - -```ts -import * as http from 'node:http' -import { createRequestListener } from 'remix/node-fetch-server' - -async function handler(request: Request) { - let url = new URL(request.url) - - if (url.pathname === '/' && request.method === 'GET') { - return new Response('Welcome to the User API! Try GET /api/users') - } - - if (url.pathname === '/api/users' && request.method === 'GET') { - return Response.json([ - { id: '1', name: 'Alice', email: 'alice@example.com' }, - { id: '2', name: 'Bob', email: 'bob@example.com' }, - ]) - } - - return new Response('Not Found', { status: 404 }) -} - -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. -`node-fetch-server` uses the `host` option when constructing `request.url`. - -```ts -import * as http from 'node:http' -import { createRequestListener } from 'remix/node-fetch-server' - -let hostname = process.env.HOST || 'api.example.com' - -async function handler(request: Request) { - console.log(request.url) // http://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 - -## Related Packages - -- [`node-serve`](https://github.com/remix-run/remix/tree/main/packages/node-serve) - - Build high-performance Fetch API servers for Node.js -- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) - - Build HTTP proxy servers using the web fetch API - -## Benchmarks - -Run the full benchmark suite: - -```sh -pnpm run bench -``` - -Update the benchmark results below: - -```sh -pnpm run bench:update-readme -``` - - - -Last updated: 2026-04-29T17:19:30.407Z - -Environment: Darwin 25.3.0, Apple M1 Pro, Node.js v24.15.0 - -Command: `wrk -t12 -c400 -d30s` - -### Raw Throughput - -Simple HTML response benchmarks without inspecting the incoming request. - -| Server | Version | Requests/sec | Avg latency | Transfer/sec | -| ------------------------- | --------: | -----------: | ----------: | -----------: | -| `remix/node-serve` | `0.0.0` | `62,225` | `6.45ms` | `9.85MB` | -| `node:http` | `24.15.0` | `47,110` | `10.66ms` | `9.66MB` | -| `remix/node-fetch-server` | `0.13.0` | `43,317` | `11.69ms` | `8.80MB` | -| `express` | `5.2.1` | `39,752` | `13.69ms` | `9.59MB` | - -### Small Body - -POST benchmarks that read and print the request method, headers, and a small -body. - -| Server | Version | Requests/sec | Avg latency | Transfer/sec | -| ------------------------- | --------: | -----------: | ----------: | -----------: | -| `remix/node-serve` | `0.0.0` | `31,213` | `12.75ms` | `4.94MB` | -| `remix/node-fetch-server` | `0.13.0` | `25,430` | `24.25ms` | `5.17MB` | -| `node:http` | `24.15.0` | `25,088` | `23.89ms` | `5.14MB` | -| `express` | `5.2.1` | `22,845` | `27.16ms` | `5.51MB` | - -### Large Body - -POST benchmarks that read and print the request method, headers, and a 1 MB -body. - -| Server | Version | Requests/sec | Avg latency | Transfer/sec | -| ------------------------- | --------: | -----------: | ----------: | -----------: | -| `remix/node-serve` | `0.0.0` | `1,148` | `327.72ms` | `186.03KB` | -| `remix/node-fetch-server` | `0.13.0` | `1,086` | `217.69ms` | `225.87KB` | -| `node:http` | `24.15.0` | `1,079` | `198.67ms` | `226.54KB` | -| `express` | `5.2.1` | `1,022` | `216.07ms` | `252.51KB` | - - - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/node-serve/index.md b/docs/agents/remix/node-serve/index.md deleted file mode 100644 index be8eb7b..0000000 --- a/docs/agents/remix/node-serve/index.md +++ /dev/null @@ -1,200 +0,0 @@ - - -# node-serve - -Build high-performance Node.js servers with web-standard Fetch API primitives. -Use this package when you want Remix-style `Request`/`Response` handlers with a -managed server optimized for production throughput. - -## Features - -- **Fetch API Handlers**: Serve standard - [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to - [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) - request handlers -- **High-Performance Node.js Server**: Start a fast managed server for Fetch API - application code -- **HTTPS Support**: Start a TLS server with certificate and key file paths -- **Managed Server Lifecycle**: Start a server with `serve()`, wait for - `server.ready`, and close it with `server.close()` -- **Custom Hostname**: Override the host and protocol used to construct incoming - `request.url` values -- **Client Info**: Access client IP address, address family, and remote port - when your handler accepts a second argument -- **Existing uWebSockets.js App Adapter**: Use `createUwsRequestHandler()` when - you already own a uWebSockets.js app - -## Installation - -```sh -npm i remix -``` - -`node-serve` includes a native high-performance transport as an optional -dependency. Standard installs include optional dependencies; if your install -disables them, enable optional dependencies before using `remix/node-serve`. - -## Usage - -Use `serve()` to start a Node.js server that calls your fetch handler for every -incoming request: - -```ts -import { serve } from 'remix/node-serve' - -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) - - if (url.pathname === '/' && request.method === 'GET') { - return new Response('Welcome to the User API! Try GET /api/users') - } - - if (url.pathname === '/api/users' && request.method === 'GET') { - return Response.json(Array.from(users.values())) - } - - 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 }) -} - -let server = serve(handler, { port: 3000 }) - -await server.ready -console.log(`Server running at http://localhost:${server.port}`) -``` - -### Custom Request URLs - -Use `host` and `protocol` when your server runs behind a proxy or load balancer -and you need stable incoming request URLs: - -```ts -import { serve } from 'remix/node-serve' - -let server = serve(handler, { - host: process.env.HOST ?? 'api.example.com', - protocol: 'https:', - port: 3000, -}) - -await server.ready -``` - -### HTTPS - -Pass `tls` options to start an HTTPS server. `keyFile` and `certFile` are file -paths, not PEM contents: - -```ts -import { serve } from 'remix/node-serve' - -let server = serve(handler, { - port: 443, - tls: { - keyFile: './certs/server.key', - certFile: './certs/server.crt', - }, -}) - -await server.ready -console.log(`Server running at https://localhost:${server.port}`) -``` - -When `tls` is present, `request.url` defaults to the `https:` protocol. You can -still set `protocol` explicitly when the public URL differs from the local -server transport. - -### Client Information - -Handlers that accept a second argument receive the remote client address: - -```ts -import { type FetchHandler, serve } from 'remix/node-serve' - -let handler: FetchHandler = async (request, client) => { - console.log(`Request from ${client.address}:${client.port}`) - - return Response.json({ - path: new URL(request.url).pathname, - clientAddress: client.address, - }) -} - -serve(handler, { port: 3000 }) -``` - -### Existing uWebSockets.js Apps - -Most apps should use `serve()`. Use `createUwsRequestHandler()` when you already -have a uWebSockets.js app and want only part of the app to use a Fetch API -handler: - -This example assumes `uWebSockets.js` is also a direct dependency of your app. - -```ts -import { App } from 'uWebSockets.js' -import { createUwsRequestHandler } from 'remix/node-serve' - -let app = App() - -async function handler(request: Request) { - let url = new URL(request.url) - return Response.json({ path: url.pathname }) -} - -app.get('/health', (res) => { - res.end('ok') -}) - -app.any('/api/*', createUwsRequestHandler(handler)) - -app.listen(3000, (socket) => { - if (!socket) throw new Error('Could not listen on port 3000') -}) -``` - -For HTTPS with an existing uWebSockets.js app, create the SSL app yourself and -pass `protocol: 'https:'` when you create the request handler: - -```ts -import { SSLApp } from 'uWebSockets.js' -import { createUwsRequestHandler } from 'remix/node-serve' - -let app = SSLApp({ - key_file_name: './certs/server.key', - cert_file_name: './certs/server.crt', -}) - -app.any('/*', createUwsRequestHandler(handler, { protocol: 'https:' })) -app.listen(443, (socket) => { - if (!socket) throw new Error('Could not listen on port 443') -}) -``` - -## Related Packages - -- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) - - Adapt Fetch API handlers to existing `node:http`, `node:https`, and - `node:http2` servers -- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - Route incoming `Request` objects to Fetch API handlers - -## Related Work - -- [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) - Web - standard `Request` and `Response` primitives - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/remix/index.md b/docs/agents/remix/remix/index.md deleted file mode 100644 index a9e4154..0000000 --- a/docs/agents/remix/remix/index.md +++ /dev/null @@ -1,59 +0,0 @@ - - -# remix - -A modern web framework for JavaScript. - -See [remix.run](https://remix.run) for more information. - -## Installation - -```sh -npm i remix -``` - -## CLI - -Create a new app with the CLI: - -```sh -npx remix@next new my-remix-app -``` - -After installing `remix`, the equivalent local command and the rest of the CLI -are available through `remix`: - -```sh -remix new my-remix-app -remix completion bash >> ~/.bashrc -remix doctor -remix doctor --fix -remix routes -remix routes --table -remix routes --table --no-headers -remix skills install -remix test -remix version -remix --no-color doctor -``` - -## Programmatic CLI - -```ts -import { runRemix } from 'remix/cli' - -await runRemix(['new', 'my-remix-app']) -await runRemix(['completion', 'bash']) -await runRemix(['doctor']) -await runRemix(['doctor', '--fix']) -await runRemix(['routes']) -await runRemix(['routes', '--table']) -await runRemix(['routes', '--table', '--no-headers']) -await runRemix(['skills', 'list']) -await runRemix(['test']) -await runRemix(['version']) -``` - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/response/index.md b/docs/agents/remix/response/index.md deleted file mode 100644 index eaa3d32..0000000 --- a/docs/agents/remix/response/index.md +++ /dev/null @@ -1,276 +0,0 @@ - - -# 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/route-pattern/index.md b/docs/agents/remix/route-pattern/index.md deleted file mode 100644 index c8df3b3..0000000 --- a/docs/agents/remix/route-pattern/index.md +++ /dev/null @@ -1,174 +0,0 @@ - - -# 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 - -## Installation - -```sh -npm i remix -``` - -## Quick Example - -```ts -import { RoutePattern } from 'remix/route-pattern' - -let blog = new RoutePattern('blog/:slug') -blog.match('https://remix.run/blog/v3') // { params: { slug: 'v3' } } -blog.href({ slug: 'v3' }) // '/blog/v3' - -let api = new RoutePattern('api(/v:version)/*path') -api.match('https://api.com/api/v2/users/profile') // { params: { version: '2', path: 'users/profile' } } -api.href({ version: '2', path: 'users/profile' }) // '/api/v2/users/profile' -api.href({ path: 'users/profile' }) // '/api/users/profile' - -let cdn = new RoutePattern('http(s)://:region.cdn.com/assets/*file.:ext') -cdn.match('https://us-west.cdn.com/assets/images/logo.png') // { params: { region: 'us-west', file: 'images/logo', ext: 'png' } } -cdn.href({ region: 'us-west', file: 'images/logo', ext: 'png' }) // 'https://us-west.cdn.com/assets/images/logo.png' -``` - -## Intuitive syntax - -**Variables** capture dynamic segments using `:name`: - -```ts -new RoutePattern('users/:id') // matches /users/123 -new RoutePattern('blog/:year-:month-:day/:slug') // matches /blog/2024-01-15/hello -``` - -**Wildcards** match multi-segment paths using `*name`: - -```ts -new RoutePattern('files/*path') // matches /files/images/logo.png -new RoutePattern('node_modules/*package/dist/index.js') // matches /node_modules/@remix-run/router/dist/index.js -new RoutePattern('files/*') // matches any path under /files, but doesn't capture the value for the wildcard -``` - -**Optionals** make parts optional using `()`: - -```ts -new RoutePattern('api(/v:version)/users') // matches /api/users AND /api/v2/users -new RoutePattern('blog/:slug(.html)') // matches /blog/hello AND /blog/hello.html -new RoutePattern('docs(/guides/:category)') // multiple segments optional: /docs OR /docs/guides/routing -new RoutePattern('api(/v:major(.:minor))') // nested optionals: /api, /api/v2, /api/v2.1 -``` - -**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') // same constraint as ?q= — key must be present -new RoutePattern('search?q=routing') // requires ?q=routing exactly -``` - -**Flexible matching** for partial URL patterns: - -```ts -new RoutePattern('blog/:slug') // omits protocol/hostname, matches any origin -new RoutePattern('://example.com/api') // omits protocol, matches http and https -new RoutePattern('search?q') // allows additional search params beyond ?q -``` - -## Matchers - -Match URLs against multiple patterns. Each pattern can have associated data -(handlers, route IDs, metadata, etc.): - -```ts -import { ArrayMatcher as Matcher } from 'remix/route-pattern' - -// Any data type you want! 👇 -let matcher = new Matcher() - -matcher.add('/', 'home') -matcher.add('blog/:slug', 'blog-post') -matcher.add('api(/v:version)/*path', 'api') - -matcher.match('https://example.com/blog/v3') -// { pattern: 'blog/:slug', params: { slug: 'v3' }, data: 'blog-post' } - -matcher.match('https://example.com/api/v2/users/profile') -// { pattern: 'api(/v:version)/*path', params: { version: '2', path: 'users/profile' }, data: 'api' } -``` - -**ArrayMatcher vs TrieMatcher** - -- **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 -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/route-pattern' -import { TrieMatcher as Matcher } from 'remix/route-pattern' -``` - -## Specificity - -When multiple patterns match a URL, the most specific pattern wins. - -**Pathname specificity** (left-to-right): - -```ts -import { ArrayMatcher } from 'remix/route-pattern' - -let matcher = new ArrayMatcher() -matcher.add('blog/hello', 'static') -matcher.add('blog/:slug', 'variable') -matcher.add('blog/*path', 'wildcard') -matcher.add('*path', 'catch-all') - -matcher.match('https://example.com/blog/hello') -// { pattern: 'blog/hello', params: {}, data: 'static' } -// 'blog/hello' wins: static segments beat variables/wildcards at each position -``` - -**Search parameter specificity**: - -```ts -let router = new ArrayMatcher() -router.add('search', 'no-params') -router.add('search?q', 'has-q') -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 (`?q` and `?q=` tie) -``` - -## Benchmark - -Benchmarks live in -[`bench/`](https://github.com/remix-run/remix/tree/remix%403.0.0-beta.0/packages/route-pattern/bench). - -## Related Work - -- [`path-to-regexp`](https://www.npmjs.com/package/path-to-regexp) -- [`find-my-way`](https://github.com/delvedor/find-my-way) -- [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/session-middleware/index.md b/docs/agents/remix/session-middleware/index.md deleted file mode 100644 index 6f4ac9b..0000000 --- a/docs/agents/remix/session-middleware/index.md +++ /dev/null @@ -1,121 +0,0 @@ - - -# 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. - -## Features - -- **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 -npm i remix -``` - -## Usage - -```ts -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! - httpOnly: true, - secure: true, - sameSite: 'lax', -}) - -let sessionStorage = createCookieSessionStorage() - -let router = createRouter({ - middleware: [session(sessionCookie, sessionStorage)], -}) - -router.get('/', (context) => { - 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.get(Session)` -- Automatically saves session changes and sets the cookie on responses - -Note: The session cookie must be signed for security. This prevents tampering -with the session data on the client. - -### Login/Logout Flow - -A basic login/logout flow could look like this: - -```ts -import * as res from 'remix/fetch-router/response-helpers' -import { Session } from 'remix/session' - -router.get('/login', ({ get }) => { - let session = get(Session) - let error = session.get('error') - return res.html(` - - -

    Login

    - ${typeof error === 'string' ?
    ${error}
    : null} -
    - - - - - - - `) -}) - -router.post('/login', ({ get }) => { - let session = get(Session) - let formData = get(FormData) - let username = formData.get('username') - let password = formData.get('password') - - let user = authenticateUser(username, password) - if (!user) { - session.flash('error', 'Invalid username or password') - return res.redirect('/login') - } - - session.regenerateId() - session.set('userId', user.id) - - return res.redirect('/dashboard') -}) - -router.post('/logout', ({ get }) => { - let session = get(Session) - session.destroy() - return res.redirect('/') -}) -``` - -## Related Packages - -- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - Router for the web Fetch API -- [`session`](https://github.com/remix-run/remix/tree/main/packages/session) - - Session management and storage -- [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) - - Cookie parsing and serialization - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/session-storage-memcache/index.md b/docs/agents/remix/session-storage-memcache/index.md deleted file mode 100644 index 18e219e..0000000 --- a/docs/agents/remix/session-storage-memcache/index.md +++ /dev/null @@ -1,44 +0,0 @@ - - -# 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/index.md b/docs/agents/remix/session-storage-redis/index.md deleted file mode 100644 index c073a99..0000000 --- a/docs/agents/remix/session-storage-redis/index.md +++ /dev/null @@ -1,40 +0,0 @@ - - -# session-storage-redis - -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-run/session @remix-run/session-storage-redis redis -``` - -## Usage - -```ts -import { createClient } from 'redis' -import { createRedisSessionStorage } from '@remix-run/session-storage-redis' - -let redis = createClient({ url: process.env.REDIS_URL }) -await redis.connect() - -let sessionStorage = createRedisSessionStorage(redis, { - keyPrefix: 'session:', - ttl: 60 * 60 * 24, -}) -``` - -## Options - -`createRedisSessionStorage(client, options)` supports: - -- `keyPrefix` (`string`, default: `'session:'`) -- `ttl` (`number` seconds) -- `useUnknownIds` (`boolean`, default: `false`) - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/session/index.md b/docs/agents/remix/session/index.md deleted file mode 100644 index b4820ac..0000000 --- a/docs/agents/remix/session/index.md +++ /dev/null @@ -1,206 +0,0 @@ - - -# 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/static-middleware/index.md b/docs/agents/remix/static-middleware/index.md deleted file mode 100644 index 156d1a6..0000000 --- a/docs/agents/remix/static-middleware/index.md +++ /dev/null @@ -1,100 +0,0 @@ - - -# static-middleware - -Static file serving middleware for Remix. Serves static files from a directory -with support for ETags, range requests, and conditional requests. - -## Features - -- [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) support - (weak and strong) -- [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 fallback to next middleware/handler if file not found - -## Installation - -```sh -npm i remix -``` - -## Usage - -Static middleware is useful for serving static files from a directory. - -```ts -import { createRouter } from 'remix/fetch-router' -import { staticFiles } from 'remix/static-middleware' - -let router = createRouter({ - middleware: [staticFiles('./public')], -}) - -router.get('/', () => new Response('Home')) -``` - -### With Cache Control - -Internally, the `staticFiles()` middleware uses the -[`createFileResponse()` helper from `@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response/index.md#file-responses) -to send files with full HTTP semantics. This means it also accepts the same -options as the `createFileResponse()` helper. - -```ts -let router = createRouter({ - middleware: [ - staticFiles('./public', { - cacheControl: 'public, max-age=31536000, immutable', // 1 year - }), - ], -}) -``` - -### Filter Files - -```ts -let router = createRouter({ - middleware: [ - staticFiles('./public', { - filter(path) { - // Don't serve hidden files - return !path.startsWith('.') - }, - }), - ], -}) -``` - -### Multiple Directories - -```ts -let router = createRouter({ - middleware: [ - staticFiles('./public'), - staticFiles('./assets', { - cacheControl: 'public, max-age=31536000', - }), - ], -}) -``` - -## Security - -- Prevents path traversal attacks (e.g., `../../../etc/passwd`) -- Only serves files with GET and HEAD requests -- Respects the configured root directory boundary - -## Related Packages - -- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - - Router for the web Fetch API -- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - - Used internally for streaming file contents - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/tar-parser/index.md b/docs/agents/remix/tar-parser/index.md deleted file mode 100644 index a94c52a..0000000 --- a/docs/agents/remix/tar-parser/index.md +++ /dev/null @@ -1,90 +0,0 @@ - - -# tar-parser - -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 - -- **Universal Runtime** - Runs anywhere JavaScript runs -- **Web Streams** - Built on the standard - [web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API), - so it's composable with `fetch()` streams -- **Format Support** - Supports POSIX, GNU, and PAX tar formats -- **Memory Efficient** - Does not buffer anything in normal usage -- **Zero Dependencies** - No external dependencies - -## Installation - -```sh -npm i remix -``` - -## Usage - -The main parser interface is the `parseTar(archive, handler)` function: - -```ts -import { parseTar } from 'remix/tar-parser' - -let response = await fetch( - 'https://github.com/remix-run/remix/archive/refs/heads/main.tar.gz', -) - -await parseTar( - response.body.pipeThrough(new DecompressionStream('gzip')), - (entry) => { - console.log(entry.name, entry.size) - }, -) -``` - -If you're parsing an archive with filename encodings other than UTF-8, use the -`filenameEncoding` option: - -```ts -let response = await fetch(/* ... */) - -await parseTar(response.body, { filenameEncoding: 'latin1' }, (entry) => { - console.log(entry.name, entry.size) -}) -``` - -## Benchmark - -`tar-parser` performs on par with other popular tar parsing libraries on -Node.js. - -``` -> @remix-run/tar-parser@0.0.0 bench /Users/michael/Projects/remix-the-web/packages/tar-parser -> 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' │ -└────────────┴────────────────────┘ -``` - -## Related Packages - -- [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) - - Fast, streaming multipart parser for JavaScript - -## Credits - -`tar-parser` is based on the excellent -[tar-stream package](https://www.npmjs.com/package/tar-stream) (MIT license) and -adopts the same core parsing algorithm, utility functions, and many test cases. - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/terminal/index.md b/docs/agents/remix/terminal/index.md deleted file mode 100644 index 8724b2c..0000000 --- a/docs/agents/remix/terminal/index.md +++ /dev/null @@ -1,107 +0,0 @@ - - -# terminal - -Terminal output utilities for JavaScript libraries and CLIs. It provides small -primitives for ANSI styles, color support detection, escape sequences, and -testable stdout/stderr handling. - -## Features - -- **ANSI Styles** - Apply common modifiers, foreground colors, and background - colors -- **Color Detection** - Respect `CI`, `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, - TTY streams, and explicit style overrides -- **Terminal Controls** - Generate escape sequences for cursor movement, line - clearing, and cursor visibility -- **Testable Streams** - Create terminal instances around injected - stdout/stderr/stdin streams - -## Installation - -```sh -npm i remix -``` - -## Usage - -```ts -import { createTerminal } from 'remix/terminal' - -let terminal = createTerminal() - -terminal.writeLine(`${terminal.styles.green('ready')} listening on port 3000`) -terminal.errorLine(terminal.styles.red('failed to start')) -``` - -### ANSI Styles - -Use `createStyles` when you only need formatting helpers. - -```ts -import { createStyles } from 'remix/terminal' - -let styles = createStyles({ colors: true }) - -console.log(styles.bold(styles.cyan('Ready'))) -console.log(styles.format('warning', 'dim', 'yellow', 'bgBlackBright')) -``` - -Style helpers preserve outer styles when nested formatted strings close an inner -style. - -```ts -console.log(styles.red(`Error: ${styles.bold('fatal')} retrying`)) -``` - -Supported modifiers include `bold`, `dim`, `italic`, `underline`, `overline`, -`inverse`, and `strikethrough`. Supported colors include the base -foreground/background ANSI colors, bright variants, and `gray`/`grey` aliases. - -By default, color detection disables styles in CI, when `NO_COLOR` is present, -for `TERM=dumb`, and outside TTY output streams. Set `colors` to `true` or -`false` to override automatic detection. - -### Terminal Controls - -Use `ansi` for raw terminal escape sequences. - -```ts -import { ansi } from 'remix/terminal' - -process.stdout.write(ansi.clearLine) -process.stdout.write(ansi.cursorTo(0)) -process.stdout.write('Updated status') -``` - -### Testing Output - -Inject streams to test terminal output without writing to the real console. - -```ts -import { createTerminal } from 'remix/terminal' - -let output = '' - -let terminal = createTerminal({ - colors: false, - stdout: { - write(chunk) { - output += chunk - }, - }, -}) - -terminal.writeLine(terminal.styles.green('ok')) -``` - -## Related Packages - -- [`logger-middleware`](https://github.com/remix-run/remix/tree/main/packages/logger-middleware) - - HTTP request/response logging middleware -- [`test`](https://github.com/remix-run/remix/tree/main/packages/test) - - Browser-based test framework for Remix components - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/test/index.md b/docs/agents/remix/test/index.md deleted file mode 100644 index fe3a8e9..0000000 --- a/docs/agents/remix/test/index.md +++ /dev/null @@ -1,476 +0,0 @@ - - -# `test` - -A test framework for JavaScript and TypeScript projects. - -## Features - -- `describe`/`it` test structure with `before`/`after`/`beforeEach`/`afterEach` - hooks -- Server-side unit testing -- Playwright E2E testing via `t.serve` -- In-browser component testing (pair with `render` from `remix/ui/test`) -- Mock functions and method spies via `t.mock.fn` / `t.mock.method` -- Unified code coverage reporting across unit and E2E tests -- Watch mode -- Config file support (`remix-test.config.ts`) - -## Installation - -```sh -npm i remix -``` - -## Usage - -Write test files that import from `remix/test`: - -```ts -import * as assert from 'remix/assert' -import { describe, it } from 'remix/test' - -describe('My Test Suite', () => { - it('tests a function', () => { - let result = something() - assert.equal(result, 42) - }) -}) -``` - -Run tests with the CLI: - -```sh -remix test -``` - -By default, `remix test` discovers all files matching -`**/*.test{,.e2e}.{ts,tsx}`. Pass one or more globs as positional arguments to -override: - -```sh -remix test "src/**/*.test.ts" -remix test "src/**/*.test.ts" "tests/**/*.test.tsx" -``` - -Or, you may control via the `glob.test` config field/CLI arg. Each `glob.*` -field accepts a single string or an array of patterns, and `--glob.*` flags can -be repeated on the CLI. - -If you install `@remix-run/test` directly instead of the umbrella `remix` -package, the same runner is available as `remix-test`: - -```sh -npm i @remix-run/test -remix-test -``` - -### Config File - -Create a `remix-test.config.ts` (or `.js`) file at the root of your project -(shown with default values): - -```ts -import type { RemixTestConfig } from 'remix/test' - -export default { - // Browser options for E2E tests - browser: { - // Echo browser console output to the terminal - echo: false, - // Open browser (via playwright `headless:false`) and keep it open after tests - // complete (useful for debugging) - open: false, - }, - - // Max number of concurrent test workers (default `os.availableParallelism()`) - concurrency: 2, - - // Pool for server and E2E test files ("forks", "threads") - pool: 'forks', - - // Code coverage options - coverage: { - // Enable coverage reporting - enabled: true, - // Output directory (default: ".coverage") - dir: '.coverage', - // Glob pattern(s) to include/exclude - include: 'src/**', - exclude: 'src/**/*.test.ts', - // Minimum thresholds (%) - statements: 80, - lines: 80, - branches: 80, - functions: 80, - }, - - // Glob pattern(s) identifying test files - glob: { - // All test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}"). - test: '**/*.test{,.browser,.e2e}.ts', - // Browser test files (default: "**/*.test.browser.{ts,tsx}") - browser: '**/*.test.browser.ts', - // E2E test files (default: "**/*.test.e2e.{ts,tsx}") - e2e: '**/*.test.e2e.ts', - }, - - // Playwright configuration for E2E tests, or string path to an existing - // config file on disk - playwrightConfig: { - projects: [ - { name: 'chromium', use: { browserName: 'chromium' } }, - { name: 'firefox', use: { browserName: 'firefox' } }, - ], - use: { - navigationTimeout: 5_000, - actionTimeout: 5_000, - }, - }, - - // Playwright project(s) to run E2E tests for - project: 'chromium', - - // Test reporter ("spec", "files", "tap", "dot") - reporter: 'spec', - - // Path to a setup module (see Setup section below) - setup: './test/setup.ts', - - // Test type(s) to run ("server", "browser", "e2e") - type: ['server', 'browser', 'e2e'], - - // Watch for file changes and re-run - watch: false, -} satisfies RemixTestConfig -``` - -### CLI Options - -You can point to a different config file location with the `--config` flag: - -```sh -remix test --config ./tests/config.ts -``` - -You may also specify any config field as a CLI flag which will take precedence -over config file values: - -| Flag | Short | -| --------------------------- | --------- | --- | -| `--browser.echo` | | -| `--browser.open` | | -| `--concurrency ` | `-c` | -| `--coverage` | | -| `--coverage.dir ` | | -| `--coverage.include` | | -| `--coverage.exclude` | | -| `--coverage.statements` | | -| `--coverage.lines` | | -| `--coverage.branches` | | -| `--coverage.functions` | | -| `--glob.test` | | -| `--glob.browser` | | -| `--glob.e2e` | | -| `--playwrightConfig ` | | -| `--pool ` | | -| `--project ` | `-p` | -| `--reporter ` | `-r` | -| `--setup ` | `-s` | -| `--type ` | `-t` | -| `--watch` | `-w` | - -### Setup - -The `setup` option points to a module that can export `globalSetup` and/or -`globalTeardown` functions, called once before and after the entire test run -respectively: - -```ts -// ./test/setup.ts -export async function globalSetup() { - await db.migrate() -} - -export async function globalTeardown() { - await db.close() -} -``` - -## API - -### Test framework - -```ts -import { - beforeAll, - afterAll, - beforeEach, - afterEach, - describe, - it, -} from 'remix/test' - -beforeAll(() => {}) -afterAll(() => {}) - -describe('My Test Suite', () => { - beforeEach(() => {}) - afterEach(() => {}) - - it('tests something', () => {}) - it('tests something else', () => {}) -}) -``` - -`suite` and `test` are aliases for `describe` and `it`. - -```ts -import { suite, test } from 'remix/test' - -suite('My Test Suite', () => { - test('tests something', () => {}) -}) -``` - -### Programmatic runner - -`@remix-run/test/cli` exports `runRemixTest()` for tools that want to run the -test runner without exiting the current process: - -```ts -import { runRemixTest } from '@remix-run/test/cli' - -let exitCode = await runRemixTest({ - argv: ['--type', 'server'], - cwd: process.cwd(), -}) -``` - -`runRemixTest()` returns the runner exit code. The `remix test` and `remix-test` -bin wrappers call `process.exit()` with that code when the run finishes so open -workers, browsers, or project handles cannot keep the CLI alive. - -### Test Context - -Each test callback receives a `TestContext` (`t`) as its first argument with -helpful test utilities. - -```ts -// from 'remix/test' -interface TestContext { - // Register a cleanup function to run after the test completes - after(fn: () => void): void - - // Mock tracker, mirroring the shape of Node's `t.mock` from `node:test` - mock: { - // Create a mock function with an optional implementation - fn any>(impl?: T): MockFunction - - // Mock an object method with an optional implementation override - method( - obj: T, - methodName: K, - impl?: Function, - ): MockFunction - } - - // Replace global timer functions with controllable fakes - useFakeTimers(): FakeTimers - - // E2E only: connect a running test server to a Playwright Page - serve(server: { baseUrl: string; close(): Promise }): Promise -} -``` - -#### Mocks and Spies - -Use `t.mock.fn()`/`t.mock.method()` to set up mocks and method spies. This is -preferred over the standalone `mock` import because TestContext method mocks are -automatically restored after the test runs. - -```ts -it('mocks and spies', (t) => { - // Create a mock function - let fn = t.mock.fn((x: number) => x * 2) - fn(3) - fn.mock.calls[0].result // 6 - - // Mock an existing method - let spy = t.mock.method(console, 'warn') - console.warn('test') - spy.mock.calls.length // 1 - // spy is restored automatically when the test ends -}) -``` - -#### Cleanup - -You can register local test cleanup logic with `t.after()`: - -```ts -it('cleanup', (t) => { - let conn = db.connect() - t.after(() => conn.close()) - // ... -}) -``` - -#### Fake Timers - -`t.useFakeTimers()` replaces the global timer functions (`setTimeout`, -`setInterval`, etc.) with controllable fakes that are automatically restored -after the test. It works in any test environment — server unit tests, browser -tests, or E2E setup code. - -```ts -it('debounces a callback', (t) => { - let timers = t.useFakeTimers() - let calls = 0 - let debounced = debounce(() => calls++, 300) - - debounced() - timers.advance(299) - assert.equal(calls, 0) - timers.advance(1) - assert.equal(calls, 1) -}) -``` - -| Method | Description | -| ------------- | --------------------------------------------------------------------------- | -| `advance(ms)` | Advance the clock by `ms` milliseconds, firing any elapsed timers | -| `restore()` | Restore the original timer functions (called automatically after each test) | - -#### E2E - -In E2E test files, `t.serve()` connects a running test server to a Playwright -`Page`. See [E2E Testing](#e2e-testing) for details. - -```ts -import { createTestServer } from 'remix/node-fetch-server/test' - -it('navigates to home', async (t) => { - let router = createRouter() - let server = await createTestServer(router.fetch) - let page = await t.serve(server) - await page.goto('/') -}) -``` - -### Standalone mocks (module scope) - -When you need a mock outside of a test body, import `mock` directly and call -`restore()` manually: - -```ts -import { mock } from 'remix/test' - -let spy = mock.method(console, 'log') -// ... -spy.mock.restore?.() -``` - -### Browser Testing - -Browser tests run components in an actual browser environment via Playwright and -are discovered by the `**/*.test.browser.{ts,tsx}` glob pattern (configurable -via `glob.browser`). They use the same `describe`/`it` API as unit tests. Each -in-browser test suite runs in an isolated `iframe` so it has access to its own -`document` instance. - -#### `render()` - -`render`, exported from `remix/ui/test`, mounts a component into the DOM and -returns a `RenderResult`: - -```ts -import * as assert from 'remix/assert' -import { describe, it } from 'remix/test' -import { render } from 'remix/ui/test' -import { Counter } from './counter.tsx' - -describe('Counter', () => { - it('increments on click', async (t) => { - let { $, act, cleanup } = render() - t.after(cleanup) - - assert.equal($('[data-count]')?.textContent, '0') - await act(() => $('[data-action="increment"]')?.click()) - assert.equal($('[data-count]')?.textContent, '1') - }) -}) -``` - -`RenderResult` provides: - -| Property/Method | Description | -| --------------- | ----------------------------------------------------------------------- | -| `container` | The `HTMLElement` the component is mounted into | -| `root` | The Remix `VirtualRoot` the component is rendered in | -| `$(selector)` | Alias for `container.querySelector()` | -| `$$(selector)` | Alias for `container.querySelectorAll()` | -| `act(fn)` | Runs `fn` and flushes pending component updates | -| `cleanup()` | Unmounts and removes the container (pass to `t.after` for auto-cleanup) | - -### E2E Testing - -End-to-end (E2E) tests use [Playwright](https://playwright.dev) and are -discovered by the `**/*.test.e2e.{ts,tsx}` glob pattern (configurable via -`glob.e2e`). They use the same `describe`/`it` API as unit tests. - -E2E tests receive `t.serve()` on the test context, which accepts a running test -server and returns a Playwright -[`Page`](https://playwright.dev/docs/api/class-page) whose `baseURL` points at -that server. The server and page are automatically closed after each test. - -```ts -import * as assert from 'remix/assert' -import { createTestServer } from 'remix/node-fetch-server/test' -import { describe, it } from 'remix/test' -import { createRouter } from './router.ts' - -describe('checkout', () => { - it('adds an item to the cart', async (t) => { - let router = createRouter() - let server = await createTestServer(router.fetch) - let page = await t.serve(server) - - await page.goto('/') - await page.getByRole('button', { name: 'Add to Cart' }).click() - await page.getByRole('link', { name: 'Cart' }).click() - await page.getByRole('heading', { name: 'Shopping Cart' }).waitFor() - - assert.equal(await page.locator('[data-test-cart-quantity]').innerText(), 1) - }) -}) -``` - -Configure Playwright (browsers, timeouts, viewport, etc.) via `playwrightConfig` -in your config file: - -```ts -export default { - playwrightConfig: { - projects: [ - { name: 'chromium', use: { browserName: 'chromium' } }, - { name: 'firefox', use: { browserName: 'firefox' } }, - { name: 'webkit', use: { browserName: 'webkit' } }, - ], - use: { - navigationTimeout: 5_000, - actionTimeout: 5_000, - }, - }, - - // Or, point to an existing playwright config file - // playwrightConfig: './playwright.config.ts' -} satisfies RemixTestConfig -``` - -Set `browser.open: true` to keep the browser open after tests finish — useful -for debugging failures. - -## License - -See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE) diff --git a/docs/agents/remix/ui/docs/component.md b/docs/agents/remix/ui/docs/component.md deleted file mode 100644 index 338835f..0000000 --- a/docs/agents/remix/ui/docs/component.md +++ /dev/null @@ -1,786 +0,0 @@ -# component - -A minimal component system built on JavaScript and DOM primitives. Write -components that render on the server, stream to the browser, and hydrate only -where you need interactivity. - -## Features - -- **JSX Runtime** - Convenient JSX syntax -- **Component State** - State managed with plain JavaScript variables -- **Manual Updates** - Explicit control over when components update via - `handle.update()` -- **Real DOM Events** - Events are real DOM events using the `on()` mixin and - `addEventListeners()` -- **Inline CSS** - `css(...)` mixin with pseudo-selectors and nested rules -- **Server Rendering** - Stream full pages or fragments with `renderToStream` -- **Hydration** - Mark interactive components with `clientEntry` and hydrate - them on the client with `run` -- **Frames** - `` streams partial server UI into the page and can be - reloaded without a full page navigation - -## Installation - -```sh -npm i remix -``` - -## Quick Start - -### Server - -Render a full page to a streaming response: - -```tsx -import { renderToStream } from 'remix/ui/server' -import { Frame } from 'remix/ui' -import { Counter } from './assets/counter.tsx' - -function App() { - return () => ( - - - My App -